Files
sam-kd/js/drawingModule.js
hskwon aca1767eb9 초기 커밋: 5130 레거시 시스템
- URL 하드코딩 → .env APP_URL 기반 동적 URL로 변경
- DB 연결 하드코딩 → .env 기반으로 변경
- MySQL strict mode DATE 오류 수정
2025-12-10 20:14:31 +09:00

967 lines
35 KiB
JavaScript

/**
* Drawing Module - 독립적인 Canvas 그리기 모듈
* @version 1.0.0
* @author 김보곤
*
* 사용법:
* const drawer = new DrawingModule({
* container: 'myContainer',
* onSave: (imageData) => { console.log('저장된 이미지:', imageData); }
* });
* drawer.show();
*/
class DrawingModule {
constructor(options = {}) {
this.options = {
container: options.container || 'body',
width: options.width || 800,
height: options.height || 600,
canvasWidth: options.canvasWidth || 320,
canvasHeight: options.canvasHeight || 240,
onSave: options.onSave || null,
onCancel: options.onCancel || null,
initialImage: options.initialImage || null,
title: options.title || '그리기 도구'
};
this.canvas = null;
this.ctx = null;
this.drawing = false;
this.drawMode = 'polyline';
this.drawColor = '#000000';
this.lineWidth = 2;
this.eraserRadius = 15;
this.isEraser = false;
this.textMode = false;
this.straightMode = true;
this.polyPrev = null;
this.startX = 0;
this.startY = 0;
this.currentPath = [];
this.drawingData = [];
this.originalImage = null;
this.modalElement = null;
this.handlers = {};
}
// 모달 생성 및 표시
show() {
this.createModal();
this.initializeCanvas();
this.attachEventHandlers();
this.modalElement.style.display = 'block';
// 초기 이미지가 있으면 로드
if (this.options.initialImage) {
this.loadImage(this.options.initialImage);
}
}
// 모달 HTML 생성
createModal() {
// 기존 모달이 있으면 제거
const existingModal = document.getElementById('drawingModuleModal');
if (existingModal) {
existingModal.remove();
}
const modalHtml = `
<div id="drawingModuleModal" class="dm-modal">
<div class="dm-modal-content">
<div class="dm-modal-header">
<h3>${this.options.title}</h3>
<span class="dm-close">&times;</span>
</div>
<div class="dm-modal-body">
<div class="dm-container">
<div class="dm-canvas-area">
<canvas id="dmCanvas"></canvas>
</div>
<div class="dm-controls">
<div class="dm-control-group">
<label>그리기 모드:</label>
<select id="dmDrawMode" class="dm-select">
<option value="polyline">점연결</option>
<option value="line">직선</option>
<option value="free">자유선</option>
</select>
</div>
<div class="dm-control-group">
<label>색상:</label>
<input type="color" id="dmDrawColor" value="#000000" class="dm-color-picker">
</div>
<div class="dm-control-group">
<label>선 굵기:</label>
<input type="range" id="dmLineWidth" min="1" max="10" value="2" class="dm-range">
<span id="dmLineWidthLabel">2px</span>
</div>
<div class="dm-control-group">
<button type="button" id="dmLineBtn" class="dm-btn dm-btn-primary">선 그리기</button>
<button type="button" id="dmTextBtn" class="dm-btn dm-btn-primary">텍스트</button>
<button type="button" id="dmEraserBtn" class="dm-btn dm-btn-warning">지우개</button>
</div>
<div class="dm-control-group" id="dmEraserSizeContainer" style="display: none;">
<label>지우개 크기:</label>
<input type="range" id="dmEraserSize" min="5" max="50" value="15" class="dm-range">
<span id="dmEraserSizeLabel">15px</span>
</div>
<div class="dm-control-group">
<label class="dm-checkbox">
<input type="checkbox" id="dmStraightMode" checked>
<span>직각 모드</span>
</label>
</div>
<div class="dm-control-group dm-button-group">
<button type="button" id="dmClearBtn" class="dm-btn dm-btn-danger">초기화</button>
<button type="button" id="dmUndoBtn" class="dm-btn dm-btn-secondary">실행취소</button>
<button type="button" id="dmSaveBtn" class="dm-btn dm-btn-success">저장</button>
<button type="button" id="dmCancelBtn" class="dm-btn dm-btn-secondary">취소</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;
// 컨테이너에 모달 추가
const container = typeof this.options.container === 'string'
? document.querySelector(this.options.container)
: this.options.container;
container.insertAdjacentHTML('beforeend', modalHtml);
this.modalElement = document.getElementById('drawingModuleModal');
}
// 캔버스 초기화
initializeCanvas() {
this.canvas = document.getElementById('dmCanvas');
this.canvas.width = this.options.canvasWidth;
this.canvas.height = this.options.canvasHeight;
// 캔버스를 포커스 가능하게 만들기
this.canvas.tabIndex = 1;
this.canvas.style.outline = 'none'; // 포커스 아웃라인 제거
this.ctx = this.canvas.getContext('2d');
this.ctx.lineWidth = this.lineWidth;
this.ctx.strokeStyle = this.drawColor;
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
// 흰색 배경 설정
this.ctx.fillStyle = 'white';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
// 이벤트 핸들러 연결
attachEventHandlers() {
// 모달 닫기 버튼
const closeBtn = this.modalElement.querySelector('.dm-close');
closeBtn.addEventListener('click', () => this.close());
// 취소 버튼
document.getElementById('dmCancelBtn').addEventListener('click', () => this.close());
// 저장 버튼
document.getElementById('dmSaveBtn').addEventListener('click', () => this.save());
// 초기화 버튼
document.getElementById('dmClearBtn').addEventListener('click', () => this.clear());
// 실행취소 버튼
document.getElementById('dmUndoBtn').addEventListener('click', () => this.undo());
// 그리기 모드 변경
document.getElementById('dmDrawMode').addEventListener('change', (e) => {
this.drawMode = e.target.value;
// 텍스트 모드 비활성화
this.textMode = false;
document.getElementById('dmTextBtn').classList.remove('dm-active');
// 지우개 모드 비활성화
this.isEraser = false;
document.getElementById('dmEraserBtn').classList.remove('dm-active');
document.getElementById('dmEraserSizeContainer').style.display = 'none';
this.removeEraserCursor();
this.canvas.style.cursor = 'crosshair';
this.resetDrawingState();
});
// 색상 변경
document.getElementById('dmDrawColor').addEventListener('change', (e) => {
this.drawColor = e.target.value;
this.ctx.strokeStyle = this.drawColor;
});
// 선 굵기 변경
document.getElementById('dmLineWidth').addEventListener('input', (e) => {
this.lineWidth = parseInt(e.target.value);
this.ctx.lineWidth = this.lineWidth;
document.getElementById('dmLineWidthLabel').textContent = `${this.lineWidth}px`;
});
// 지우개 버튼
document.getElementById('dmEraserBtn').addEventListener('click', (e) => {
e.preventDefault(); // 기본 동작 방지
e.stopPropagation(); // 이벤트 버블링 방지
this.toggleEraser();
});
// 지우개 크기 변경
document.getElementById('dmEraserSize').addEventListener('input', (e) => {
this.eraserRadius = parseInt(e.target.value);
document.getElementById('dmEraserSizeLabel').textContent = `${this.eraserRadius}px`;
// 지우개 모드일 때 커서 크기 업데이트
if (this.isEraser) {
const cursor = document.getElementById('dmEraserCursor');
if (cursor) {
cursor.style.width = `${this.eraserRadius * 2}px`;
cursor.style.height = `${this.eraserRadius * 2}px`;
}
}
});
// 직각 모드
document.getElementById('dmStraightMode').addEventListener('change', (e) => {
this.straightMode = e.target.checked;
});
// 선 그리기 버튼
document.getElementById('dmLineBtn').addEventListener('click', () => {
this.drawMode = 'line';
document.getElementById('dmDrawMode').value = 'line';
// 텍스트 모드 비활성화
this.textMode = false;
document.getElementById('dmTextBtn').classList.remove('dm-active');
// 지우개 모드 비활성화
this.isEraser = false;
document.getElementById('dmEraserBtn').classList.remove('dm-active');
document.getElementById('dmEraserSizeContainer').style.display = 'none';
this.removeEraserCursor();
this.canvas.style.cursor = 'crosshair';
this.resetDrawingState();
});
// 텍스트 버튼
document.getElementById('dmTextBtn').addEventListener('click', () => {
this.textMode = !this.textMode;
if (this.textMode) {
// 텍스트 모드 활성화
document.getElementById('dmTextBtn').classList.add('dm-active');
this.canvas.style.cursor = 'text';
// 다른 모드 비활성화
this.isEraser = false;
this.drawing = false;
this.polyPrev = null;
document.getElementById('dmEraserBtn').classList.remove('dm-active');
document.getElementById('dmEraserSizeContainer').style.display = 'none';
this.removeEraserCursor();
} else {
// 텍스트 모드 비활성화
document.getElementById('dmTextBtn').classList.remove('dm-active');
this.canvas.style.cursor = 'crosshair';
}
});
// 캔버스 이벤트
this.attachCanvasEvents();
}
// 캔버스 이벤트 처리
attachCanvasEvents() {
// 마우스 이벤트
this.canvas.addEventListener('mousedown', (e) => this.handleMouseDown(e));
this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e));
this.canvas.addEventListener('mouseup', (e) => this.handleMouseUp(e));
this.canvas.addEventListener('mouseout', (e) => this.handleMouseOut(e));
// 터치 이벤트 (모바일 지원)
this.canvas.addEventListener('touchstart', (e) => this.handleTouchStart(e));
this.canvas.addEventListener('touchmove', (e) => this.handleTouchMove(e));
this.canvas.addEventListener('touchend', (e) => this.handleTouchEnd(e));
// ESC 키 이벤트 (점연결 모드 취소)
this.handleEscKey = (e) => {
if (e.key === 'Escape' && this.drawMode === 'polyline' && this.polyPrev) {
this.cancelPolyline();
}
};
document.addEventListener('keydown', this.handleEscKey);
}
// 마우스 다운 이벤트
handleMouseDown(e) {
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 캔버스 밖에서 클릭한 경우 무시
if (x < 0 || x > this.canvas.width || y < 0 || y > this.canvas.height) {
return;
}
if (this.textMode) {
this.addText(x, y);
return; // 텍스트 모드는 유지됨
}
if (this.isEraser) {
this.drawing = true;
this.eraseAt(x, y);
return;
}
switch (this.drawMode) {
case 'polyline':
this.handlePolylineClick(x, y);
break;
case 'line':
this.startLine(x, y);
break;
case 'free':
this.startFreeDraw(x, y);
break;
}
}
// 마우스 이동 이벤트
handleMouseMove(e) {
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (this.isEraser && this.drawing) {
this.eraseAt(x, y);
return;
}
if (this.drawing) {
switch (this.drawMode) {
case 'line':
this.previewLine(x, y);
break;
case 'free':
this.continueDraw(x, y);
break;
}
}
// polyline 모드에서 미리보기
if (this.drawMode === 'polyline' && this.polyPrev) {
this.previewPolyline(x, y);
}
}
// 마우스 업 이벤트
handleMouseUp(e) {
if (this.isEraser) {
this.drawing = false;
this.saveState();
return;
}
if (this.drawing && this.drawMode === 'line') {
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
this.endLine(x, y);
} else if (this.drawing && this.drawMode === 'free') {
this.endFreeDraw();
}
}
// 마우스 아웃 이벤트
handleMouseOut(e) {
if (this.drawing && this.drawMode === 'free') {
this.endFreeDraw();
}
}
// 터치 이벤트 (모바일)
handleTouchStart(e) {
e.preventDefault();
const touch = e.touches[0];
const mouseEvent = new MouseEvent('mousedown', {
clientX: touch.clientX,
clientY: touch.clientY
});
this.canvas.dispatchEvent(mouseEvent);
}
handleTouchMove(e) {
e.preventDefault();
const touch = e.touches[0];
const mouseEvent = new MouseEvent('mousemove', {
clientX: touch.clientX,
clientY: touch.clientY
});
this.canvas.dispatchEvent(mouseEvent);
}
handleTouchEnd(e) {
e.preventDefault();
const mouseEvent = new MouseEvent('mouseup', {});
this.canvas.dispatchEvent(mouseEvent);
}
// Polyline 그리기
handlePolylineClick(x, y) {
if (!this.polyPrev) {
// 첫 번째 점
this.polyPrev = { x, y };
this.currentPath = [{ x, y }];
this.saveState(); // 시작 시점 상태 저장
} else {
// 연결선 그리기
let endX = x;
let endY = y;
if (this.straightMode) {
const angle = Math.atan2(y - this.polyPrev.y, x - this.polyPrev.x);
const angleDeg = Math.abs(angle * 180 / Math.PI);
if (angleDeg < 22.5 || angleDeg > 157.5) {
endY = this.polyPrev.y; // 수평선
} else if (angleDeg > 67.5 && angleDeg < 112.5) {
endX = this.polyPrev.x; // 수직선
}
}
this.ctx.beginPath();
this.ctx.moveTo(this.polyPrev.x, this.polyPrev.y);
this.ctx.lineTo(endX, endY);
this.ctx.stroke();
this.currentPath.push({ x: endX, y: endY });
this.polyPrev = { x: endX, y: endY };
this.saveState(); // 각 선분 그린 후 상태 저장
}
}
// Polyline 미리보기
previewPolyline(x, y) {
// 이전 상태 복원 (잔상 제거)
if (this.drawingData.length > 0) {
const img = new Image();
img.onload = () => {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.drawImage(img, 0, 0);
// 미리보기 선 그리기
let endX = x;
let endY = y;
if (this.straightMode) {
const angle = Math.atan2(y - this.polyPrev.y, x - this.polyPrev.x);
const angleDeg = Math.abs(angle * 180 / Math.PI);
if (angleDeg < 22.5 || angleDeg > 157.5) {
endY = this.polyPrev.y;
} else if (angleDeg > 67.5 && angleDeg < 112.5) {
endX = this.polyPrev.x;
}
}
this.ctx.save();
this.ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
this.ctx.setLineDash([5, 5]);
this.ctx.beginPath();
this.ctx.moveTo(this.polyPrev.x, this.polyPrev.y);
this.ctx.lineTo(endX, endY);
this.ctx.stroke();
this.ctx.restore();
};
img.src = this.drawingData[this.drawingData.length - 1];
}
}
// 직선 그리기
startLine(x, y) {
this.drawing = true;
this.startX = x;
this.startY = y;
this.saveState();
}
previewLine(x, y) {
// 이전 상태 복원 (미리보기용)
if (this.drawingData.length > 0) {
const img = new Image();
img.onload = () => {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.drawImage(img, 0, 0);
// 미리보기 선 그리기
let endX = x;
let endY = y;
if (this.straightMode) {
const angle = Math.atan2(y - this.startY, x - this.startX);
const angleDeg = Math.abs(angle * 180 / Math.PI);
if (angleDeg < 22.5 || angleDeg > 157.5) {
endY = this.startY;
} else if (angleDeg > 67.5 && angleDeg < 112.5) {
endX = this.startX;
}
}
this.ctx.save();
this.ctx.strokeStyle = this.drawColor;
this.ctx.lineWidth = this.lineWidth;
this.ctx.beginPath();
this.ctx.moveTo(this.startX, this.startY);
this.ctx.lineTo(endX, endY);
this.ctx.stroke();
this.ctx.restore();
};
img.src = this.drawingData[this.drawingData.length - 1];
}
}
endLine(x, y) {
// 최종 선 그리기
let endX = x;
let endY = y;
if (this.straightMode) {
const angle = Math.atan2(y - this.startY, x - this.startX);
const angleDeg = Math.abs(angle * 180 / Math.PI);
if (angleDeg < 22.5 || angleDeg > 157.5) {
endY = this.startY;
} else if (angleDeg > 67.5 && angleDeg < 112.5) {
endX = this.startX;
}
}
// 먼저 이전 상태 복원
if (this.drawingData.length > 0) {
const img = new Image();
img.onload = () => {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.drawImage(img, 0, 0);
// 최종 선 그리기
this.ctx.save();
this.ctx.strokeStyle = this.drawColor;
this.ctx.lineWidth = this.lineWidth;
this.ctx.beginPath();
this.ctx.moveTo(this.startX, this.startY);
this.ctx.lineTo(endX, endY);
this.ctx.stroke();
this.ctx.restore();
// 그린 후 상태 저장
this.drawing = false;
this.saveState();
};
img.src = this.drawingData[this.drawingData.length - 1];
} else {
// 저장된 상태가 없는 경우 직접 그리기
this.ctx.save();
this.ctx.strokeStyle = this.drawColor;
this.ctx.lineWidth = this.lineWidth;
this.ctx.beginPath();
this.ctx.moveTo(this.startX, this.startY);
this.ctx.lineTo(endX, endY);
this.ctx.stroke();
this.ctx.restore();
this.drawing = false;
this.saveState();
}
}
// 자유 그리기
startFreeDraw(x, y) {
this.drawing = true;
this.ctx.beginPath();
this.ctx.moveTo(x, y);
}
continueDraw(x, y) {
this.ctx.lineTo(x, y);
this.ctx.stroke();
}
endFreeDraw() {
this.drawing = false;
this.saveState();
}
// 지우개 기능
toggleEraser() {
this.isEraser = !this.isEraser;
const eraserBtn = document.getElementById('dmEraserBtn');
const eraserContainer = document.getElementById('dmEraserSizeContainer');
if (this.isEraser) {
eraserBtn.classList.add('dm-active');
eraserContainer.style.display = 'block';
this.canvas.style.cursor = 'none';
// 다른 그리기 모드 비활성화
this.textMode = false;
this.drawing = false;
this.polyPrev = null;
document.getElementById('dmTextBtn').classList.remove('dm-active');
// 지우개 커서 생성 (포커스 관련 문제 방지를 위해 마지막에)
this.createEraserCursor();
} else {
eraserBtn.classList.remove('dm-active');
eraserContainer.style.display = 'none';
// 텍스트 모드가 아니면 crosshair 커서로
this.canvas.style.cursor = this.textMode ? 'text' : 'crosshair';
this.removeEraserCursor();
}
}
eraseAt(x, y) {
this.ctx.save();
this.ctx.globalCompositeOperation = 'destination-out';
this.ctx.beginPath();
this.ctx.arc(x, y, this.eraserRadius, 0, Math.PI * 2);
this.ctx.fill();
this.ctx.restore();
}
// 지우개 커서
createEraserCursor() {
// 기존 커서가 있으면 먼저 제거
this.removeEraserCursor();
let cursor = document.createElement('div');
cursor.id = 'dmEraserCursor';
cursor.className = 'dm-eraser-cursor';
cursor.style.width = `${this.eraserRadius * 2}px`;
cursor.style.height = `${this.eraserRadius * 2}px`;
cursor.style.pointerEvents = 'none'; // 커서가 마우스 이벤트를 방해하지 않도록
cursor.style.position = 'absolute';
cursor.style.display = 'none';
cursor.style.zIndex = '999'; // 커서가 충분히 위에 나타나도록
cursor.style.borderRadius = '50%'; // 원형 모양
this.modalElement.querySelector('.dm-canvas-area').appendChild(cursor);
// 이벤트 리스너 바인딩 저장 (중복 방지를 위해 기존 것 제거 후 추가)
this.eraserCursorMove = (e) => this.updateEraserCursor(e);
this.eraserCursorEnter = (e) => {
const cursor = document.getElementById('dmEraserCursor');
if (cursor && this.isEraser) {
this.updateEraserCursor(e); // 진입 시 위치 업데이트
cursor.style.display = 'block';
}
};
this.eraserCursorLeave = () => {
const cursor = document.getElementById('dmEraserCursor');
if (cursor) {
cursor.style.display = 'none';
}
};
this.canvas.addEventListener('mousemove', this.eraserCursorMove, { passive: true });
this.canvas.addEventListener('mouseenter', this.eraserCursorEnter, { passive: true });
this.canvas.addEventListener('mouseleave', this.eraserCursorLeave, { passive: true });
}
updateEraserCursor(e) {
const cursor = document.getElementById('dmEraserCursor');
if (!cursor || !this.isEraser) return;
const canvasRect = this.canvas.getBoundingClientRect();
const canvasAreaRect = this.modalElement.querySelector('.dm-canvas-area').getBoundingClientRect();
// 캔버스 영역 상대 좌표 계산
const canvasX = e.clientX - canvasRect.left;
const canvasY = e.clientY - canvasRect.top;
// 캔버스 영역 대비 좌표 계산 (커서 위치용)
const areaX = e.clientX - canvasAreaRect.left;
const areaY = e.clientY - canvasAreaRect.top;
// 커서 크기 업데이트
cursor.style.width = `${this.eraserRadius * 2}px`;
cursor.style.height = `${this.eraserRadius * 2}px`;
// 캔버스 내부에 있을 때만 커서 표시
if (canvasX >= 0 && canvasX <= this.canvas.width && canvasY >= 0 && canvasY <= this.canvas.height) {
cursor.style.left = `${areaX - this.eraserRadius}px`;
cursor.style.top = `${areaY - this.eraserRadius}px`;
cursor.style.display = 'block';
} else {
cursor.style.display = 'none';
}
}
removeEraserCursor() {
// 이벤트 리스너 제거
if (this.canvas) {
if (this.eraserCursorMove) {
this.canvas.removeEventListener('mousemove', this.eraserCursorMove);
}
if (this.eraserCursorEnter) {
this.canvas.removeEventListener('mouseenter', this.eraserCursorEnter);
}
if (this.eraserCursorLeave) {
this.canvas.removeEventListener('mouseleave', this.eraserCursorLeave);
}
}
// 커서 엘리먼트 제거
const cursor = document.getElementById('dmEraserCursor');
if (cursor) {
cursor.remove();
}
}
// 텍스트 추가
addText(x, y) {
const text = prompt('텍스트를 입력하세요:');
if (text) {
this.ctx.save();
this.ctx.font = '14px Arial';
this.ctx.fillStyle = this.drawColor;
this.ctx.fillText(text, x, y);
this.ctx.restore();
this.saveState();
}
// 텍스트 모드 유지 (사용자가 명시적으로 다른 모드를 선택할 때까지)
// this.textMode = false; // 이 줄을 제거하여 텍스트 모드 유지
// document.getElementById('dmTextBtn').classList.remove('dm-active'); // 버튼 활성 상태 유지
this.canvas.style.cursor = 'text'; // 텍스트 커서 유지
}
// 상태 저장/복원
saveState() {
const imageData = this.canvas.toDataURL();
this.drawingData.push(imageData);
if (this.drawingData.length > 20) {
this.drawingData.shift();
}
}
restoreState() {
if (this.drawingData.length > 0) {
const img = new Image();
img.onload = () => {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.drawImage(img, 0, 0);
};
img.src = this.drawingData[this.drawingData.length - 1];
}
}
redrawCanvas() {
if (this.drawingData.length > 0) {
const img = new Image();
img.onload = () => {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.drawImage(img, 0, 0);
};
img.src = this.drawingData[this.drawingData.length - 1];
}
}
// 실행취소
undo() {
if (this.drawingData.length > 1) {
this.drawingData.pop();
this.restoreState();
} else if (this.drawingData.length === 1) {
this.drawingData.pop();
this.clear();
}
}
// 초기화
clear() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.fillStyle = 'white';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.drawingData = [];
this.resetDrawingState();
}
// 그리기 상태 초기화
resetDrawingState() {
this.drawing = false;
this.polyPrev = null;
this.currentPath = [];
this.isEraser = false;
this.textMode = false;
document.getElementById('dmEraserBtn').classList.remove('dm-active');
document.getElementById('dmTextBtn').classList.remove('dm-active');
document.getElementById('dmEraserSizeContainer').style.display = 'none';
this.canvas.style.cursor = 'crosshair';
this.removeEraserCursor();
// 잔상 제거를 위한 캔버스 다시 그리기
if (this.drawingData.length > 0) {
this.restoreState();
}
}
// Polyline 취소 (ESC 키)
cancelPolyline() {
this.polyPrev = null;
this.currentPath = [];
// 마지막 저장 상태로 복원
if (this.drawingData.length > 0) {
const img = new Image();
img.onload = () => {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.drawImage(img, 0, 0);
};
img.src = this.drawingData[this.drawingData.length - 1];
} else {
// 저장된 상태가 없으면 캔버스 초기화
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.fillStyle = 'white';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
console.log('Polyline 그리기 취소됨');
}
// 이미지 로드
loadImage(imageSrc) {
const img = new Image();
img.onload = () => {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// 캔버스 크기에 맞게 이미지 조정
const scale = Math.min(
this.canvas.width / img.width,
this.canvas.height / img.height
);
const width = img.width * scale;
const height = img.height * scale;
const x = (this.canvas.width - width) / 2;
const y = (this.canvas.height - height) / 2;
this.ctx.drawImage(img, x, y, width, height);
this.originalImage = this.canvas.toDataURL();
this.saveState();
};
img.src = imageSrc;
}
// 저장
save() {
// JPG로 저장하기 위해 흰색 배경이 있는 임시 캔버스 생성
const tempCanvas = document.createElement('canvas');
tempCanvas.width = this.canvas.width;
tempCanvas.height = this.canvas.height;
const tempCtx = tempCanvas.getContext('2d');
// 흰색 배경 먼저 그리기
tempCtx.fillStyle = '#FFFFFF';
tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
// 기존 캔버스 내용을 위에 그리기
tempCtx.drawImage(this.canvas, 0, 0);
// JPG 형식으로 데이터 생성 (품질 0.9)
const imageData = tempCanvas.toDataURL('image/jpeg', 0.9);
if (this.options.onSave) {
this.options.onSave({
imageData: imageData,
timestamp: new Date().toISOString(),
width: this.canvas.width,
height: this.canvas.height,
format: 'jpeg'
});
}
this.close();
}
// 닫기
close() {
if (this.options.onCancel && !this.saved) {
this.options.onCancel();
}
// ESC 키 이벤트 리스너 제거
if (this.handleEscKey) {
document.removeEventListener('keydown', this.handleEscKey);
}
this.removeEraserCursor();
this.modalElement.remove();
this.modalElement = null;
this.canvas = null;
this.ctx = null;
}
// 이미지 데이터 가져오기 (JPG 형식)
getImageData(format = 'jpeg', quality = 0.9) {
if (format === 'jpeg') {
// JPG의 경우 흰색 배경 추가
const tempCanvas = document.createElement('canvas');
tempCanvas.width = this.canvas.width;
tempCanvas.height = this.canvas.height;
const tempCtx = tempCanvas.getContext('2d');
// 흰색 배경 먼저 그리기
tempCtx.fillStyle = '#FFFFFF';
tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
// 기존 캔버스 내용을 위에 그리기
tempCtx.drawImage(this.canvas, 0, 0);
return tempCanvas.toDataURL('image/jpeg', quality);
} else {
// PNG는 기존 방식
return this.canvas.toDataURL('image/png');
}
}
// 이미지 다운로드 (기본 JPG)
download(filename = 'drawing.jpg', format = 'jpeg', quality = 0.9) {
const link = document.createElement('a');
link.download = filename;
if (format === 'jpeg') {
// JPG의 경우 흰색 배경 추가
const tempCanvas = document.createElement('canvas');
tempCanvas.width = this.canvas.width;
tempCanvas.height = this.canvas.height;
const tempCtx = tempCanvas.getContext('2d');
// 흰색 배경 먼저 그리기
tempCtx.fillStyle = '#FFFFFF';
tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
// 기존 캔버스 내용을 위에 그리기
tempCtx.drawImage(this.canvas, 0, 0);
link.href = tempCanvas.toDataURL('image/jpeg', quality);
} else {
// PNG는 기존 방식
link.href = this.canvas.toDataURL('image/png');
}
link.click();
}
}
// 전역 네임스페이스에 등록
window.DrawingModule = DrawingModule;