- URL 하드코딩 → .env APP_URL 기반 동적 URL로 변경 - DB 연결 하드코딩 → .env 기반으로 변경 - MySQL strict mode DATE 오류 수정
273 lines
9.5 KiB
JavaScript
273 lines
9.5 KiB
JavaScript
export default class DrawingTool {
|
|
constructor({
|
|
container, fileInput,
|
|
drawBtn, lineBtn, textBtn, eraserBtn,
|
|
saveBtn, clearBtn, modeSelect, colorPicker,
|
|
straightToggle, eraserSize, eraserSizeLabel
|
|
}) {
|
|
// DOM elements
|
|
this.container = container;
|
|
this.fileInput = fileInput; // <input type="file"> for saving
|
|
this.drawBtn = drawBtn;
|
|
this.lineBtn = lineBtn;
|
|
this.textBtn = textBtn;
|
|
this.eraserBtn = eraserBtn;
|
|
this.saveBtn = saveBtn;
|
|
this.clearBtn = clearBtn;
|
|
this.modeSelect = modeSelect;
|
|
this.colorPicker = colorPicker;
|
|
this.straightToggle = straightToggle;
|
|
this.eraserSize = eraserSize;
|
|
this.eraserSizeLabel = eraserSizeLabel;
|
|
|
|
// internal state
|
|
this.canvas = null;
|
|
this.ctx = null;
|
|
this.drawingData = [];
|
|
this.currentPath = [];
|
|
this.isDrawing = false;
|
|
this.drawMode = 'polyline';
|
|
this.drawColor = colorPicker.value || '#000';
|
|
this.eraserRadius = parseInt(eraserSize.value, 10) || 15;
|
|
this.polyPrev = null;
|
|
this.previewLine = null;
|
|
|
|
// bind handlers
|
|
this._onMouseDown = this._onMouseDown.bind(this);
|
|
this._onMouseMove = this._onMouseMove.bind(this);
|
|
this._onMouseUp = this._onMouseUp.bind(this);
|
|
this._onMouseOut = this._onMouseOut.bind(this);
|
|
this._onCanvasClick = this._onCanvasClick.bind(this);
|
|
this._onKeyDown = this._onKeyDown.bind(this);
|
|
}
|
|
|
|
init() {
|
|
// UI controls
|
|
this.drawBtn.addEventListener('click', () => this._startDrawing());
|
|
this.lineBtn.addEventListener('click', () => this._setMode('polyline', this.lineBtn));
|
|
this.textBtn.addEventListener('click', () => this._setMode('text', this.textBtn));
|
|
this.eraserBtn.addEventListener('click',() => this._setMode('eraser', this.eraserBtn));
|
|
this.modeSelect.addEventListener('change',() => this._setMode(this.modeSelect.value));
|
|
this.colorPicker.addEventListener('input',() => this.drawColor = this.colorPicker.value);
|
|
this.straightToggle.addEventListener('change', () => {});
|
|
this.eraserSize.addEventListener('input', () => {
|
|
this.eraserRadius = parseInt(this.eraserSize.value, 10);
|
|
this.eraserSizeLabel.textContent = this.eraserSize.value + 'px';
|
|
});
|
|
|
|
// action buttons
|
|
this.clearBtn.addEventListener('click', () => this._clearCanvas());
|
|
this.saveBtn.addEventListener('click', () => this._saveDrawing());
|
|
}
|
|
|
|
_startDrawing() {
|
|
// reset mode to polyline
|
|
this._setMode('polyline', this.lineBtn);
|
|
if (this.canvas) return;
|
|
|
|
// create overlay canvas
|
|
this.canvas = document.createElement('canvas');
|
|
this.canvas.classList.add('drawing-canvas');
|
|
this.canvas.width = this.container.clientWidth;
|
|
this.canvas.height = this.container.clientHeight;
|
|
this.container.appendChild(this.canvas);
|
|
this.ctx = this.canvas.getContext('2d');
|
|
this.ctx.lineWidth = 2;
|
|
|
|
// draw background image or white
|
|
const img = this.container.querySelector('img');
|
|
if (img && img.complete) {
|
|
this.ctx.drawImage(img, 0, 0, this.canvas.width, this.canvas.height);
|
|
} else if (!img) {
|
|
this.ctx.fillStyle = '#fff';
|
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
}
|
|
|
|
// bind events
|
|
this.canvas.addEventListener('mousedown', this._onMouseDown);
|
|
this.canvas.addEventListener('mousemove', this._onMouseMove);
|
|
this.canvas.addEventListener('mouseup', this._onMouseUp);
|
|
this.canvas.addEventListener('mouseout', this._onMouseOut);
|
|
this.canvas.addEventListener('click', this._onCanvasClick);
|
|
document.addEventListener('keydown', this._onKeyDown);
|
|
|
|
// show controls
|
|
[this.lineBtn, this.textBtn, this.eraserBtn,
|
|
this.modeSelect, this.colorPicker,
|
|
this.straightToggle,
|
|
this.saveBtn, this.clearBtn,
|
|
this.eraserSize, this.eraserSizeLabel]
|
|
.forEach(el => el.style.display = 'inline-block');
|
|
}
|
|
|
|
_setMode(mode, activeBtn) {
|
|
this.drawMode = mode;
|
|
this.modeSelect.value = mode;
|
|
[this.lineBtn, this.textBtn, this.eraserBtn].forEach(btn => btn.classList.remove('active'));
|
|
if (activeBtn) activeBtn.classList.add('active');
|
|
}
|
|
|
|
_onMouseDown(e) {
|
|
if (this.drawMode === 'polyline') return;
|
|
this.isDrawing = true;
|
|
const {x,y} = this._getXY(e);
|
|
if (this.drawMode === 'free') {
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(x,y);
|
|
this.currentPath = [{x,y}];
|
|
} else {
|
|
this.startX = x;
|
|
this.startY = y;
|
|
}
|
|
}
|
|
|
|
_onMouseMove(e) {
|
|
const {x,y} = this._getXY(e);
|
|
if (this.drawMode === 'polyline' && this.polyPrev) {
|
|
this._drawPreviewLine(x,y);
|
|
}
|
|
if (!this.isDrawing) return;
|
|
if (this.drawMode === 'eraser') {
|
|
this.ctx.save();
|
|
this.ctx.globalCompositeOperation = 'destination-out';
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(x,y,this.eraserRadius,0,2*Math.PI);
|
|
this.ctx.fill();
|
|
this.ctx.restore();
|
|
} else if (this.drawMode === 'free') {
|
|
this.ctx.strokeStyle = this.drawColor;
|
|
this.ctx.lineTo(x,y);
|
|
this.ctx.stroke();
|
|
this.currentPath.push({x,y});
|
|
}
|
|
}
|
|
|
|
_onMouseUp(e) {
|
|
if (!this.isDrawing) return;
|
|
const {x,y} = this._getXY(e);
|
|
if (this.drawMode === 'line') {
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(this.startX, this.startY);
|
|
this.ctx.lineTo(x,y);
|
|
this.ctx.strokeStyle = this.drawColor;
|
|
this.ctx.stroke();
|
|
this._saveData('line', {startX:this.startX, startY:this.startY, endX:x, endY:y});
|
|
} else if (this.drawMode === 'free' && this.currentPath.length >1) {
|
|
this._saveData('free', {path: [...this.currentPath]});
|
|
}
|
|
this.isDrawing = false;
|
|
this.currentPath = [];
|
|
}
|
|
|
|
_onMouseOut() {
|
|
this.isDrawing = false;
|
|
if (this.drawMode==='polyline' && this.previewLine) {
|
|
this._redrawAll();
|
|
this.previewLine = null;
|
|
}
|
|
}
|
|
|
|
_onCanvasClick(e) {
|
|
if (this.drawMode === 'text') return; // text input handled separately
|
|
if (this.drawMode !== 'polyline') return;
|
|
const {x:rawX,y:rawY} = this._getXY(e);
|
|
let endX=rawX, endY=rawY;
|
|
if (this.straightToggle.checked && this.polyPrev) {
|
|
const dx=rawX-this.polyPrev.x, dy=rawY-this.polyPrev.y;
|
|
if (Math.abs(dx)>Math.abs(dy)) endY=this.polyPrev.y; else endX=this.polyPrev.x;
|
|
}
|
|
if (!this.polyPrev) {
|
|
this.polyPrev={x:endX,y:endY};
|
|
return;
|
|
}
|
|
this._redrawAll();
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(this.polyPrev.x, this.polyPrev.y);
|
|
this.ctx.lineTo(endX,endY);
|
|
this.ctx.strokeStyle = this.drawColor;
|
|
this.ctx.stroke();
|
|
this._saveData('polyline', {startX:this.polyPrev.x, startY:this.polyPrev.y, endX, endY});
|
|
this.polyPrev={x:endX,y:endY};
|
|
}
|
|
|
|
_onKeyDown(e) {
|
|
if (e.key==='Escape' && this.drawMode==='polyline') {
|
|
this.polyPrev = null;
|
|
this._redrawAll();
|
|
this.previewLine = null;
|
|
}
|
|
}
|
|
|
|
_getXY(e) {
|
|
const r = this.canvas.getBoundingClientRect();
|
|
return {x: e.clientX - r.left, y: e.clientY - r.top};
|
|
}
|
|
|
|
_drawPreviewLine(x,y) {
|
|
this._redrawAll();
|
|
this.ctx.save();
|
|
this.ctx.strokeStyle = this.drawColor;
|
|
this.ctx.globalAlpha = 0.3;
|
|
this.ctx.setLineDash([5,5]);
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(this.polyPrev.x, this.polyPrev.y);
|
|
this.ctx.lineTo(x,y);
|
|
this.ctx.stroke();
|
|
this.ctx.restore();
|
|
this.previewLine={startX:this.polyPrev.x,startY:this.polyPrev.y,endX:x,endY:y};
|
|
}
|
|
|
|
_saveData(type, data) {
|
|
this.drawingData.push({type, data, color:this.drawColor, lineWidth:this.ctx.lineWidth});
|
|
}
|
|
|
|
_redrawAll() {
|
|
// clear then draw background
|
|
this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height);
|
|
const img = this.container.querySelector('img');
|
|
if (img && img.complete) this.ctx.drawImage(img,0,0,this.canvas.width,this.canvas.height);
|
|
else {
|
|
this.ctx.fillStyle='#fff'; this.ctx.fillRect(0,0,this.canvas.width,this.canvas.height);
|
|
}
|
|
// redraw shapes
|
|
for (const item of this.drawingData) {
|
|
this.ctx.save();
|
|
this.ctx.strokeStyle = item.color;
|
|
this.ctx.lineWidth = item.lineWidth;
|
|
this.ctx.setLineDash([]);
|
|
if (item.type==='line' || item.type==='polyline') {
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(item.data.startX, item.data.startY);
|
|
this.ctx.lineTo(item.data.endX, item.data.endY);
|
|
this.ctx.stroke();
|
|
} else if (item.type==='free') {
|
|
const p0 = item.data.path[0];
|
|
this.ctx.beginPath(); this.ctx.moveTo(p0.x,p0.y);
|
|
item.data.path.slice(1).forEach(p=> this.ctx.lineTo(p.x,p.y));
|
|
this.ctx.stroke();
|
|
}
|
|
this.ctx.restore();
|
|
}
|
|
}
|
|
|
|
_clearCanvas() {
|
|
if (!this.canvas) return;
|
|
this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height);
|
|
this._redrawAll();
|
|
this.drawingData=[];
|
|
this.polyPrev=null;
|
|
this.previewLine=null;
|
|
}
|
|
|
|
_saveDrawing() {
|
|
if (!this.canvas) return;
|
|
this._redrawAll();
|
|
this.canvas.toBlob(blob=>{
|
|
const file = new File([blob],'drawing.png',{type:'image/png'});
|
|
const dt = new DataTransfer();
|
|
dt.items.add(file);
|
|
if (this.fileInput) this.fileInput.files = dt.files;
|
|
});
|
|
}
|
|
}
|
|
|