- URL 하드코딩 → .env APP_URL 기반 동적 URL로 변경 - DB 연결 하드코딩 → .env 기반으로 변경 - MySQL strict mode DATE 오류 수정
512 lines
17 KiB
JavaScript
512 lines
17 KiB
JavaScript
// imageEditor.js
|
|
export function openImageEditor(initialSrc) {
|
|
console.log('=== openImageEditor 함수 시작 ===');
|
|
console.log('받은 이미지 소스:', initialSrc);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
console.log('Promise 생성됨');
|
|
|
|
const dialog = document.getElementById('editorDialog');
|
|
const canvasEl = document.getElementById('c');
|
|
const applyBtn = document.getElementById('applyBtn');
|
|
const closeBtn = document.getElementById('closeEditorBtn');
|
|
|
|
console.log('DOM 요소 확인:', {
|
|
dialog: !!dialog,
|
|
canvasEl: !!canvasEl,
|
|
applyBtn: !!applyBtn,
|
|
closeBtn: !!closeBtn
|
|
});
|
|
|
|
if (!dialog) {
|
|
console.error('editorDialog 요소를 찾을 수 없습니다!');
|
|
reject(new Error('Dialog element not found'));
|
|
return;
|
|
}
|
|
|
|
if (!canvasEl) {
|
|
console.error('canvas 요소를 찾을 수 없습니다!');
|
|
reject(new Error('Canvas element not found'));
|
|
return;
|
|
}
|
|
|
|
// 캔버스 초기화 또는 생성
|
|
let canvas;
|
|
if (!openImageEditor._initialized) {
|
|
// 최초 초기화
|
|
canvas = new fabric.Canvas(canvasEl, { selection: false });
|
|
openImageEditor._canvas = canvas;
|
|
openImageEditor._initialized = true;
|
|
|
|
// Fabric 텍스트 baseline 호환성 패치
|
|
fabric.Text.prototype.textBaseline = 'alphabetic';
|
|
if (fabric.IText) fabric.IText.prototype.textBaseline = 'alphabetic';
|
|
} else {
|
|
// 기존 캔버스 재사용 시 초기화
|
|
canvas = openImageEditor._canvas;
|
|
console.log('기존 캔버스 초기화 중...');
|
|
|
|
// 모든 객체 제거 (배경 이미지 제외)
|
|
const objects = canvas.getObjects();
|
|
objects.forEach(obj => {
|
|
if (obj !== canvas.backgroundImage) {
|
|
canvas.remove(obj);
|
|
}
|
|
});
|
|
|
|
// 캔버스 상태 초기화
|
|
canvas.clear();
|
|
canvas.renderAll();
|
|
console.log('캔버스 초기화 완료');
|
|
}
|
|
|
|
// 상태 변수 (캔버스 초기화와 관계없이 항상 실행)
|
|
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(b => b.classList.remove('active'));
|
|
const idMap = {
|
|
polyline: 'polyBtn', free: 'freeBtn', line: 'lineBtn',
|
|
text: 'textBtn', eraser: 'eraserBtn', select: 'selectBtn'
|
|
};
|
|
const btnId = idMap[mode];
|
|
if (btnId) {
|
|
const btn = document.getElementById(btnId);
|
|
if (btn) btn.classList.add('active');
|
|
}
|
|
}
|
|
|
|
// 모드 전환
|
|
function setMode(m) {
|
|
// polyline 모드 진입 시 초기화
|
|
if (m === 'polyline') {
|
|
polyPoints = [];
|
|
isPreview = true;
|
|
if (previewLine) {
|
|
canvas.remove(previewLine);
|
|
previewLine = null;
|
|
}
|
|
isLine = false;
|
|
lineObj = null;
|
|
} else if (mode === 'polyline') {
|
|
// polyline 모드에서 다른 모드로 전환 시 임시 그리기 초기화
|
|
if (previewLine) {
|
|
canvas.remove(previewLine);
|
|
previewLine = null;
|
|
}
|
|
polyPoints = [];
|
|
}
|
|
mode = m;
|
|
canvas.isDrawingMode = (m === 'free' || m === 'eraser');
|
|
if (m === 'free') canvas.freeDrawingBrush = freeBrush;
|
|
if (m === 'eraser') canvas.freeDrawingBrush = eraserBrush;
|
|
selectMode = (m === 'select');
|
|
canvas.selection = selectMode;
|
|
canvas.upperCanvasEl.style.cursor = (m === 'eraser' ? canvas.freeDrawingCursor : 'crosshair');
|
|
// 직각 고정 상태에 따른 툴팁 설정
|
|
if ((m === 'polyline' || m === 'line') && isRight) {
|
|
canvas.upperCanvasEl.title = '직각 고정 ON - 수평/수직 선만 그려집니다';
|
|
} else {
|
|
canvas.upperCanvasEl.title = '';
|
|
}
|
|
canvas.getObjects().forEach(o => {
|
|
if (o !== canvas.backgroundImage) {
|
|
o.selectable = selectMode;
|
|
o.evented = selectMode;
|
|
}
|
|
});
|
|
highlight();
|
|
}
|
|
setMode('polyline');
|
|
|
|
// 툴바 클릭 바인딩 (modeMap으로 명확히 매핑)
|
|
const modeMap = {
|
|
polyBtn: 'polyline',
|
|
freeBtn: 'free',
|
|
lineBtn: 'line',
|
|
textBtn: 'text',
|
|
eraserBtn: 'eraser',
|
|
selectBtn: 'select'
|
|
};
|
|
Object.entries(modeMap).forEach(([btnId, m]) => {
|
|
const el = document.getElementById(btnId);
|
|
if (el) el.addEventListener('click', () => setMode(m));
|
|
});
|
|
|
|
// 전체 초기화 버튼
|
|
const clearBtn = document.getElementById('clearBtn');
|
|
if (clearBtn) {
|
|
clearBtn.addEventListener('click', () => {
|
|
canvas.clear();
|
|
canvas.setBackgroundImage(null, canvas.renderAll.bind(canvas));
|
|
polyPoints = [];
|
|
if (previewLine) { canvas.remove(previewLine); previewLine = null; }
|
|
isLine = false; lineObj = null; isPreview = true;
|
|
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.dataset.color;
|
|
freeBrush.color = currentColor;
|
|
});
|
|
});
|
|
|
|
// 지우개 크기 변경
|
|
const eraserRange = document.getElementById('eraserRange');
|
|
if (eraserRange) {
|
|
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;
|
|
});
|
|
}
|
|
|
|
// 직각 고정 토글
|
|
const rightAngle = document.getElementById('rightAngle');
|
|
if (rightAngle) {
|
|
// 직각 고정 상태 업데이트 함수
|
|
function updateRightAngleState() {
|
|
isRight = rightAngle.checked;
|
|
console.log('직각 고정:', isRight ? 'ON' : 'OFF');
|
|
|
|
// 직각 고정 상태에 따른 커서 변경
|
|
if (mode === 'polyline' || mode === 'line') {
|
|
if (isRight) {
|
|
canvas.upperCanvasEl.style.cursor = 'crosshair';
|
|
// 직각 고정 활성화 시 시각적 피드백
|
|
canvas.upperCanvasEl.title = '직각 고정 ON - 수평/수직 선만 그려집니다';
|
|
} else {
|
|
canvas.upperCanvasEl.style.cursor = 'crosshair';
|
|
canvas.upperCanvasEl.title = '';
|
|
}
|
|
}
|
|
}
|
|
|
|
// change 이벤트 리스너
|
|
rightAngle.addEventListener('change', updateRightAngleState);
|
|
|
|
// input 이벤트 리스너 (모든 상태 변경 감지)
|
|
rightAngle.addEventListener('input', function(e) {
|
|
console.log('checkbox input 이벤트 발생');
|
|
updateRightAngleState();
|
|
});
|
|
|
|
// click 이벤트 리스너 (직접 클릭 시 강제 토글)
|
|
rightAngle.addEventListener('click', function(e) {
|
|
console.log('checkbox click 이벤트 발생');
|
|
// 기본 동작 후 상태 업데이트
|
|
setTimeout(() => {
|
|
updateRightAngleState();
|
|
}, 5);
|
|
});
|
|
|
|
// 초기 상태 설정
|
|
updateRightAngleState();
|
|
}
|
|
|
|
// ESC 키: 가상선만 취소, 다이얼로그 닫히지 않음
|
|
window.addEventListener('keydown', e => {
|
|
if (!dialog.open) return;
|
|
if (e.key !== 'Escape') return;
|
|
e.preventDefault();
|
|
|
|
// 텍스트 입력 중이면 textarea 제거
|
|
const texts = document.querySelectorAll('.canvas-text-input');
|
|
if (texts.length) {
|
|
texts.forEach(el => el.remove());
|
|
return;
|
|
}
|
|
// 2) polyline 모드일 때만 previewLine 제거 및 상태 초기화
|
|
if (mode === 'polyline') {
|
|
if (previewLine) {
|
|
canvas.remove(previewLine);
|
|
previewLine = null;
|
|
}
|
|
// 내부 점 배열과 프리뷰 플래그 초기화
|
|
polyPoints = [];
|
|
isPreview = true;
|
|
|
|
// 모드를 재진입시켜 완전 초기화
|
|
setMode('polyline');
|
|
|
|
canvas.requestRenderAll();
|
|
return;
|
|
}
|
|
// 그 외 ESC 로는 아무 동작 없음
|
|
});
|
|
|
|
// 마우스 이벤트
|
|
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') {
|
|
if (polyPoints.length === 0) 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);
|
|
const 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) {
|
|
canvas.add(new fabric.Text(txt, { left: p.x, top: p.y, fontSize:14, fill:currentColor }
|
|
));
|
|
}
|
|
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);
|
|
const 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 > 0 && isPreview) {
|
|
const L = polyPoints[polyPoints.length - 1];
|
|
let x2 = p.x, y2 = p.y;
|
|
|
|
// 폴리라인 모드의 직각 고정
|
|
if (isRight) {
|
|
const dx = Math.abs(x2 - L.x);
|
|
const 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; }
|
|
});
|
|
|
|
canvas.on('path:created', e => {
|
|
e.path.selectable = selectMode;
|
|
e.path.stroke = (mode === 'eraser') ? '#ffffff' : currentColor;
|
|
});
|
|
|
|
// 이미지 로드 및 다이얼로그 표시
|
|
console.log('이미지 로드 시작...');
|
|
openImageEditor._lastSrc = initialSrc;
|
|
|
|
// 이미지 소스가 유효하지 않거나 빈 경우 새로운 빈 캔버스 생성
|
|
if (!initialSrc || initialSrc === 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2Y5ZjlmOSIvPjwvc3ZnPg==' || initialSrc.includes('placeholder')) {
|
|
console.log('유효하지 않은 이미지 소스, 새로운 빈 캔버스 생성');
|
|
const canvas = openImageEditor._canvas;
|
|
console.log('캔버스 설정:', canvas);
|
|
|
|
// 캔버스 초기화 - 기존 그리기 내용 제거
|
|
console.log('캔버스 초기화 중...');
|
|
canvas.clear();
|
|
|
|
// 기본 크기로 설정 (400x300)
|
|
canvas.setWidth(400);
|
|
canvas.setHeight(300);
|
|
|
|
// 흰색 배경 설정
|
|
canvas.setBackgroundColor('#ffffff', canvas.renderAll.bind(canvas));
|
|
|
|
console.log('빈 캔버스 초기화 완료');
|
|
|
|
console.log('dialog.showModal() 호출 전');
|
|
console.log('dialog 객체:', dialog);
|
|
console.log('dialog.showModal 타입:', typeof dialog.showModal);
|
|
|
|
try {
|
|
dialog.showModal();
|
|
console.log('dialog.showModal() 호출 성공');
|
|
} catch (error) {
|
|
console.error('dialog.showModal() 호출 실패:', error);
|
|
reject(error);
|
|
return;
|
|
}
|
|
|
|
// Prevent dialog from closing on button click by forcing type="button"
|
|
dialog.querySelectorAll('button').forEach(btn => btn.type = 'button');
|
|
dialog.focus();
|
|
console.log('dialog 설정 완료');
|
|
} else {
|
|
// 유효한 이미지가 있는 경우 기존 로직 실행
|
|
fabric.Image.fromURL(initialSrc, img => {
|
|
console.log('이미지 로드 완료:', img);
|
|
img.selectable = false;
|
|
// 캔버스 크기를 이미지 크기에 맞춤 (1:1 비율 유지)
|
|
const canvas = openImageEditor._canvas;
|
|
console.log('캔버스 설정:', canvas);
|
|
|
|
// 캔버스 초기화 - 기존 그리기 내용 제거
|
|
console.log('캔버스 초기화 중...');
|
|
canvas.clear();
|
|
|
|
canvas.setWidth(img.width);
|
|
canvas.setHeight(img.height);
|
|
// 이미지를 원본 크기 그대로 배경으로 설정
|
|
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), {
|
|
originX: 'left',
|
|
originY: 'top',
|
|
left: 0,
|
|
top: 0,
|
|
scaleX: 1,
|
|
scaleY: 1
|
|
});
|
|
|
|
console.log('캔버스 초기화 완료');
|
|
|
|
console.log('dialog.showModal() 호출 전');
|
|
console.log('dialog 객체:', dialog);
|
|
console.log('dialog.showModal 타입:', typeof dialog.showModal);
|
|
|
|
try {
|
|
dialog.showModal();
|
|
console.log('dialog.showModal() 호출 성공');
|
|
} catch (error) {
|
|
console.error('dialog.showModal() 호출 실패:', error);
|
|
reject(error);
|
|
return;
|
|
}
|
|
|
|
// Prevent dialog from closing on button click by forcing type="button"
|
|
dialog.querySelectorAll('button').forEach(btn => btn.type = 'button');
|
|
dialog.focus();
|
|
console.log('dialog 설정 완료');
|
|
}, { crossOrigin: 'anonymous' });
|
|
}
|
|
|
|
// 적용 버튼
|
|
applyBtn.addEventListener('click', () => {
|
|
const dataURL = openImageEditor._canvas.toDataURL('png');
|
|
resolve(dataURL);
|
|
dialog.close();
|
|
});
|
|
|
|
// 닫기 버튼
|
|
closeBtn.addEventListener('click', () => {
|
|
reject(new Error('User cancelled'));
|
|
dialog.close();
|
|
});
|
|
|
|
// dialog 기본 ESC 닫기 방지
|
|
dialog.addEventListener('cancel', e => e.preventDefault());
|
|
// dialog 외부 클릭 시 닫기 방지
|
|
dialog.addEventListener('mousedown', e => {
|
|
if (e.target === dialog) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
});
|
|
// dialog 내부 클릭 시에도 닫힘 방지 (버튼 제외)
|
|
dialog.addEventListener('click', e => {
|
|
if (e.target === dialog) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
});
|
|
});
|
|
}
|