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