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

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();
}
});
});
}