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

267 lines
11 KiB
JavaScript

// imageEditor.js
export function openImageEditor(initialSrc) {
return new Promise((resolve, reject) => {
const dialog = document.getElementById('editorDialog');
const canvasEl = document.getElementById('c');
const applyBtn = document.getElementById('applyBtn');
const closeBtn = document.getElementById('closeEditorBtn');
// 1회성 초기화
if (!openImageEditor._initialized) {
const canvas = new fabric.Canvas(canvasEl, { selection: false });
openImageEditor._canvas = canvas;
// 상태 변수
let mode = 'polyline';
let selectMode = false;
let isLine = false;
let lineObj = null;
let polyPoints = [];
let previewLine = null;
let isPreview = true;
let isRight = true;
let currentColor = '#000000';
// 브러시 설정
const freeBrush = canvas.freeDrawingBrush;
freeBrush.width = 2;
freeBrush.color = currentColor;
const eraserBrush = new fabric.PencilBrush(canvas);
eraserBrush.width = +document.getElementById('eraserRange').value;
eraserBrush.color = '#ffffff';
function updateEraserCursor(r) {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${r}" height="${r}"><circle cx="${r/2}" cy="${r/2}" r="${r/2}" stroke="black" fill="none"/></svg>`;
canvas.freeDrawingCursor = `url("data:image/svg+xml,${encodeURIComponent(svg)}") ${r/2} ${r/2}, auto`;
}
updateEraserCursor(eraserBrush.width);
// 버튼 강조
function highlight() {
document.querySelectorAll('.toolbar-btn').forEach(btn => btn.classList.remove('active'));
switch (mode) {
case 'polyline': document.getElementById('polyBtn').classList.add('active'); break;
case 'free': document.getElementById('freeBtn').classList.add('active'); break;
case 'line': document.getElementById('lineBtn').classList.add('active'); break;
case 'text': document.getElementById('textBtn').classList.add('active'); break;
case 'eraser': document.getElementById('eraserBtn').classList.add('active'); break;
case 'select': document.getElementById('selectBtn').classList.add('active'); break;
}
}
// 모드 전환
function setMode(m) {
if (mode === 'polyline' && m !== 'polyline') {
if (previewLine) { canvas.remove(previewLine); previewLine = null; }
polyPoints = [];
}
mode = m;
canvas.isDrawingMode = false;
if (m === 'free') {
canvas.isDrawingMode = true;
canvas.freeDrawingBrush = freeBrush;
}
if (m === 'eraser') {
canvas.isDrawingMode = true;
canvas.freeDrawingBrush = eraserBrush;
}
selectMode = (m === 'select');
canvas.selection = selectMode;
canvas.upperCanvasEl.style.cursor = (m === 'eraser') ? canvas.freeDrawingCursor : 'crosshair';
canvas.getObjects().forEach(o => {
if (o === canvas.backgroundImage) return;
o.selectable = selectMode;
o.evented = selectMode;
});
highlight();
}
setMode('polyline');
// 툴바 버튼 이벤트
document.getElementById('polyBtn').addEventListener('click', () => setMode('polyline'));
document.getElementById('freeBtn').addEventListener('click', () => setMode('free'));
document.getElementById('lineBtn').addEventListener('click', () => setMode('line'));
document.getElementById('textBtn').addEventListener('click', () => setMode('text'));
document.getElementById('eraserBtn').addEventListener('click', () => setMode('eraser'));
document.getElementById('selectBtn').addEventListener('click', () =>
setMode(document.getElementById('selectBtn').classList.contains('active') ? 'free' : 'select')
);
document.getElementById('clearBtn').addEventListener('click', () => {
canvas.clear();
fabric.Image.fromURL(openImageEditor._lastSrc, img => {
img.selectable = false;
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
});
polyPoints = [];
if (previewLine) { canvas.remove(previewLine); previewLine = null; }
canvas.upperCanvasEl.style.cursor = 'crosshair';
setMode('polyline');
});
// 색상 선택
document.querySelectorAll('#colorPicker .color-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#colorPicker .color-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentColor = btn.getAttribute('data-color');
freeBrush.color = currentColor;
});
});
// 지우개 크기
document.getElementById('eraserRange').addEventListener('input', e => {
const r = +e.target.value;
document.getElementById('eraserSizeLabel').innerText = r;
eraserBrush.width = r;
updateEraserCursor(r);
if (mode === 'eraser') canvas.upperCanvasEl.style.cursor = canvas.freeDrawingCursor;
});
// 직각 고정
document.getElementById('rightAngle').addEventListener('change', e => {
isRight = e.target.checked;
});
// 키보드 단축키
document.addEventListener('keydown', e => {
if (e.key.toLowerCase() === 'l') setMode('line');
if (e.key === 'Escape' && mode === 'polyline') {
isPreview = false;
if (previewLine) { canvas.remove(previewLine); previewLine = null; canvas.requestRenderAll(); }
}
if (e.key === 'Escape' && mode === 'text') {
document.querySelectorAll('.canvas-text-input').forEach(el => el.remove());
}
if (e.key === 'Delete' && selectMode) {
canvas.getActiveObjects().forEach(o => canvas.remove(o));
canvas.discardActiveObject();
canvas.requestRenderAll();
}
});
// 마우스 이벤트
canvas.on('mouse:down', opt => {
const p = canvas.getPointer(opt.e);
if (mode === 'line') {
isLine = true;
lineObj = new fabric.Line([p.x, p.y, p.x, p.y], {
stroke: currentColor,
strokeWidth: 2,
originX: 'center', originY: 'center',
selectable: selectMode
});
canvas.add(lineObj);
} else if (mode === 'polyline') {
isPreview = true;
let x = p.x, y = p.y;
if (isRight && polyPoints.length) {
const prev = polyPoints[polyPoints.length - 1];
const dx = Math.abs(x - prev.x), dy = Math.abs(y - prev.y);
if (dx > dy) y = prev.y; else x = prev.x;
}
if (polyPoints.length) {
const prev = polyPoints[polyPoints.length - 1];
const seg = new fabric.Line([prev.x, prev.y, x, y], {
stroke: currentColor,
strokeWidth: 2,
originX: 'center', originY: 'center',
selectable: selectMode
});
canvas.add(seg);
}
polyPoints.push({ x, y });
if (previewLine) { canvas.remove(previewLine); previewLine = null; }
} else if (mode === 'text') {
document.querySelectorAll('.canvas-text-input').forEach(el => el.remove());
const ta = document.createElement('textarea');
ta.className = 'canvas-text-input';
ta.style.left = `${p.x}px`; ta.style.top = `${p.y}px`;
ta.rows = 1; ta.cols = 20; ta.style.fontSize = '14px';
document.getElementById('editorBody').appendChild(ta);
setTimeout(() => ta.focus(), 0);
ta.addEventListener('keydown', ev => {
if (ev.key === 'Enter') {
ev.preventDefault();
const txt = ta.value.trim();
if (txt) {
const t = new fabric.Text(txt, { left: p.x, top: p.y, fontSize: 14, fill: currentColor });
canvas.add(t);
}
ta.remove();
} else if (ev.key === 'Escape') ta.remove();
});
}
});
canvas.on('mouse:move', opt => {
const p = canvas.getPointer(opt.e);
if (mode === 'line' && isLine) {
let x2 = p.x, y2 = p.y;
if (isRight) {
const dx = Math.abs(x2 - lineObj.x1), dy = Math.abs(y2 - lineObj.y1);
if (dx > dy) y2 = lineObj.y1; else x2 = lineObj.x1;
}
lineObj.set({ x2, y2 });
canvas.requestRenderAll();
} else if (mode === 'polyline' && polyPoints.length && isPreview) {
const L = polyPoints[polyPoints.length - 1];
let x2 = p.x, y2 = p.y;
if (isRight) {
const dx = Math.abs(x2 - L.x), dy = Math.abs(y2 - L.y);
if (dx > dy) y2 = L.y; else x2 = L.x;
}
if (previewLine) {
previewLine.set({ x1: L.x, y1: L.y, x2, y2 });
} else {
previewLine = new fabric.Line([L.x, L.y, x2, y2], {
stroke: 'gray', strokeWidth: 1, strokeDashArray: [5,5], selectable: false
});
canvas.add(previewLine);
}
canvas.requestRenderAll();
}
});
canvas.on('mouse:up', () => {
if (mode === 'line') {
isLine = false;
lineObj = null;
}
});
// path 생성 시 스타일
canvas.on('path:created', e => {
e.path.selectable = selectMode;
e.path.stroke = (mode === 'eraser') ? '#ffffff' : currentColor;
});
openImageEditor._initialized = true;
}
// 이미지 로드 및 dialog 표시
openImageEditor._lastSrc = initialSrc;
fabric.Image.fromURL(initialSrc, img => {
img.selectable = false;
openImageEditor._canvas.setBackgroundImage(img, openImageEditor._canvas.renderAll.bind(openImageEditor._canvas));
dialog.showModal();
});
// 적용 버튼
applyBtn.addEventListener('click', () => {
const dataURL = openImageEditor._canvas.toDataURL({ format: 'png' });
resolve(dataURL);
dialog.close();
});
// 닫기 버튼
closeBtn.addEventListener('click', () => {
reject(new Error('User cancelled'));
dialog.close();
});
// ESC로 닫기 방지
dialog.addEventListener('cancel', e => e.preventDefault());
});
}