- URL 하드코딩 → .env APP_URL 기반 동적 URL로 변경 - DB 연결 하드코딩 → .env 기반으로 변경 - MySQL strict mode DATE 오류 수정
267 lines
11 KiB
JavaScript
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());
|
|
});
|
|
}
|
|
|