/* ========== 전역 변수 및 기본 설정 ========== */ // 이미지별 효과 상태를 관리하는 Map const imageEffectsMap = new Map(); let selectedImage = null; let savedToolbarPosition = null; // 초기 효과 상태 const defaultEffects = { brightness: 100, contrast: 100, saturation: 100, blur: 0, grayscale: 0, invert: 0, opacity: 100, sepia: 0, shadowX: 0, shadowY: 0, shadowBlur: 0, shadowColor: 'rgba(0,0,0,0)', radius: 0, borderWidth: 0, borderColor: 'rgba(0,0,0,1)' }; /* ========== 유틸리티 함수 ========== */ /** * 슬라이더 ID에서 효과 키를 추출하는 함수 * ex) brightness-slider → brightness */ function getEffectKeyFromSliderId(sliderId) { return sliderId.replace('-slider', '').replace(/-(.)/g, (_, char) => char.toUpperCase()); } /* ========== 이미지 효과 적용 함수 ========== */ /** * 선택된 이미지에 현재 설정된 효과들을 적용하는 함수 */ function applyImageEffects() { if (!selectedImage) return; const effects = imageEffectsMap.get(selectedImage[0]); const filter = ` brightness(${effects.brightness}%) contrast(${effects.contrast}%) saturate(${effects.saturation}%) grayscale(${effects.grayscale}%) invert(${effects.invert}%) opacity(${effects.opacity}%) sepia(${effects.sepia}%) blur(${effects.blur}px) drop-shadow(${effects.shadowX}px ${effects.shadowY}px ${effects.shadowBlur}px ${effects.shadowColor}) `; selectedImage.css({ filter: filter.trim(), 'border-radius': `${effects.radius}px`, 'border-width': effects.borderWidth + 'px', 'border-style': 'solid', 'border-color': effects.borderColor }); } function extractEffectsFromInlineStyle($img) { const effects = { ...defaultEffects }; const styleAttr = $img.attr('style'); // ✅ 인라인 스타일 직접 가져오기 if (styleAttr && styleAttr.includes('filter')) { const filterMatch = styleAttr.match(/filter:\s*([^;]+)/); if (filterMatch) { const filter = filterMatch[1]; effects.brightness = getFilterValue(filter, 'brightness', '%', 100); effects.contrast = getFilterValue(filter, 'contrast', '%', 100); effects.saturation = getFilterValue(filter, 'saturate', '%', 100); effects.grayscale = getFilterValue(filter, 'grayscale', '%', 0); effects.invert = getFilterValue(filter, 'invert', '%', 0); effects.opacity = getFilterValue(filter, 'opacity', '%', 100); effects.sepia = getFilterValue(filter, 'sepia', '%', 0); effects.blur = getFilterValue(filter, 'blur', 'px', 0); // ✅ drop-shadow() 값 추출 const dropShadowMatch = filter.match(/drop-shadow\(rgba?\((.*?)\)\s+([\d.]+)px\s+([\d.]+)px\s+([\d.]+)px\)/); if (dropShadowMatch) { effects.shadowColor = `rgba(${dropShadowMatch[1]})`; effects.shadowX = parseFloat(dropShadowMatch[2]); effects.shadowY = parseFloat(dropShadowMatch[3]); effects.shadowBlur = parseFloat(dropShadowMatch[4]); } } } // ✅ border 관련 속성도 인라인 스타일에서 가져오기 effects.radius = getInlineStyleValue($img, 'border-radius', 'px', 0); effects.borderWidth = getInlineStyleValue($img, 'border-width', 'px', 0); effects.borderColor = getInlineBorderColor($img) || 'rgba(0,0,0,1)'; return effects; } function getFilterValue(filterStr, property, unit, defaultValue) { const match = filterStr.match(new RegExp(`${property}\\(([-\\d.]+)${unit}\\)`)); return match ? parseFloat(match[1]) : defaultValue; } function getInlineStyleValue($img, property, unit, defaultValue) { const style = $img.attr('style'); if (!style) return defaultValue; const match = style.match(new RegExp(`${property}:\\s*([-\\d.]+)${unit}`)); return match ? parseFloat(match[1]) : defaultValue; } function getInlineBorderColor($img) { const style = $img.attr('style'); if (!style) return null; const match = style.match(/border-color:\s*(rgb[a]?\([^)]*\))/); return match ? match[1] : null; } /* ========== 이벤트 핸들러 등록 ========== */ // 이미지 슬라이더 변화에 따른 효과 업데이트 $('.image-slider-ui').on('input', function () { if (!selectedImage) return; // 현재 선택된 이미지의 효과 상태 가져오기 const effects = imageEffectsMap.get(selectedImage[0]); const effectKey = getEffectKeyFromSliderId($(this).attr('id')); // 효과 상태 업데이트 effects[effectKey] = $(this).val(); applyImageEffects(); // 이미지에 효과 적용 }); // 드롭섀도우 색상 변경 이벤트 핸들러 $('#shadow-color-slider').on('input', function () { if (!selectedImage) return; const transparency = $(this).val(); // 0~100 사이의 값 const effects = imageEffectsMap.get(selectedImage[0]); if (!effects) return; // alpha 값을 슬라이더 값에 비례하게 계산하여 그림자 컬러 업데이트 effects.shadowColor = `rgba(0, 0, 0, ${transparency / 100})`; applyImageEffects(); // 변경된 효과를 이미지에 적용 }); // 테두리 색상 변경 이벤트 핸들러 $('#border-color-slider').on('input', function () { if (!selectedImage) return; const transparency = $(this).val(); // 0~100 사이의 값 const effects = imageEffectsMap.get(selectedImage[0]); if (!effects) return; // alpha 값을 투명도로 계산하여 borderColor 업데이트 effects.borderColor = `rgba(0, 0, 0, ${transparency / 100})`; applyImageEffects(); // 변경된 효과를 이미지에 적용 }); // 효과 초기화 버튼 클릭 이벤트 핸들러 $('#reset-effects-btn').click(function () { if (!selectedImage) return; // 선택된 이미지의 효과 상태를 기본값으로 초기화 const effects = imageEffectsMap.get(selectedImage[0]); Object.assign(effects, defaultEffects); // 슬라이더와 효과 상태 동기화 syncSlidersWithEffects(effects); applyImageEffects(); }); /* ========== 이미지 선택 및 툴바 관련 함수 ========== */ /** * 이미지를 선택했을 때 호출되는 함수 * 선택된 이미지에 대한 효과 상태를 불러오고, 슬라이더 UI와 동기화합니다. */ function selectImage(image) { selectedImage = image; let effects = imageEffectsMap.get(image[0]); if (!effects) { effects = extractEffectsFromInlineStyle(image); imageEffectsMap.set(image[0], effects); } syncSlidersWithEffects(effects); } function syncSlidersWithEffects(effects) { $('#brightness-slider').val(effects.brightness); $('#contrast-slider').val(effects.contrast); $('#saturation-slider').val(effects.saturation); $('#blur-slider').val(effects.blur); $('#grayscale-slider').val(effects.grayscale); $('#invert-slider').val(effects.invert); $('#opacity-slider').val(effects.opacity); $('#sepia-slider').val(effects.sepia); $('#shadow-x-slider').val(effects.shadowX); $('#shadow-y-slider').val(effects.shadowY); $('#shadow-blur-slider').val(effects.shadowBlur); $('#radius-slider').val(effects.radius); $('#border-width-slider').val(effects.borderWidth); // ✅ shadowColor (rgba -> alpha 값만 슬라이더에 적용) if (effects.shadowColor) { const shadowMatch = effects.shadowColor.match(/rgba?\(\d+,\s*\d+,\s*\d+,\s*([\d.]+)\)/); if (shadowMatch) { $('#shadow-color-slider').val(parseFloat(shadowMatch[1]) * 100); // 0~1 -> 0~100 변환 } } // ✅ borderColor (rgba -> alpha 값만 슬라이더에 적용) if (effects.borderColor) { const borderMatch = effects.borderColor.match(/rgba?\(\d+,\s*\d+,\s*\d+,\s*([\d.]+)\)/); if (borderMatch) { $('#border-color-slider').val(parseFloat(borderMatch[1]) * 100); // 0~1 -> 0~100 변환 } } } /** * 슬라이더 조정 시 이미지 툴바를 계속 표시하는 함수 */ function keepImageToolbarVisible() { if (selectedImage) { $('#image-toolbar').fadeIn(0); // 슬라이더 조정 시 툴바 표시 유지 } } $('#editor').on('mousedown', '.resizable', function (e) { // 크기 조절 핸들이 아닌 영역에서 커서 방지 const isHandle = $(e.target).hasClass('resize-handle'); if (!isHandle) { e.preventDefault(); // 기본 동작 방지 } // `.resize-handle`이 없는 경우 추가 if ($(this).children('.resize-handle').length === 0) { $(this).append('
'); // 새로 추가된 핸들에 대해 커스텀 리사이징 이벤트를 다시 바인딩 makeImageResizableWithObserver($(this)); } }); $(document).on('mousedown touchstart', '.resizable img', function(e) { // 터치 이벤트의 경우, 두 손가락 이상이면 panning 동작 실행하지 않음 if (e.type === 'touchstart' && e.touches.length > 1) { return; } // 리사이즈 핸들 내부에서 발생한 이벤트는 무시하여 충돌 방지 if ($(e.target).closest('.resize-handle').length > 0) { return; } e.preventDefault(); var $img = $(this); // 부모 영역에 꽉 차도록 설정 (빈 공간 방지) $img.css({ 'width': '100%' }); // 시작 좌표 (마우스/터치 구분) var startX = e.type === 'mousedown' ? e.clientX : e.touches[0].clientX; var startY = e.type === 'mousedown' ? e.clientY : e.touches[0].clientY; // .resizable (부모) 크기 var $parent = $img.closest('.resizable'); var containerWidth = $parent.width(); var containerHeight = $parent.height(); // 이전 드래그에서 저장된 object-position이 있다면 사용, 없으면 기본값 50% 50% var storedPos = $img.data('initialObjPos'); var initialObjPos = storedPos ? storedPos : window.getComputedStyle($img[0]).objectPosition; var initialX = 50, initialY = 50; if (initialObjPos && initialObjPos.split(' ').length === 2) { var parts = initialObjPos.split(' '); var parsedX = parseFloat(parts[0]); var parsedY = parseFloat(parts[1]); initialX = isNaN(parsedX) ? 50 : parsedX; initialY = isNaN(parsedY) ? 50 : parsedY; } // 이미지 자연 크기 var naturalWidth = $img[0].naturalWidth; var naturalHeight = $img[0].naturalHeight; // object-fit: cover 적용 시, 실제 렌더링되는 이미지 크기 계산 var scale = Math.max(containerWidth / naturalWidth, containerHeight / naturalHeight); var displayedWidth = naturalWidth * scale; var displayedHeight = naturalHeight * scale; // 컨테이너 대비 이미지의 여분 (panning 가능한 범위) var extraWidth = displayedWidth - containerWidth; var extraHeight = displayedHeight - containerHeight; var dragData = { startX, startY, initialX, initialY, extraWidth, extraHeight }; function onMove(e) { var moveX = e.type === 'mousemove' ? e.clientX : e.touches[0].clientX; var moveY = e.type === 'mousemove' ? e.clientY : e.touches[0].clientY; var dx = moveX - dragData.startX; var dy = moveY - dragData.startY; var newX = dragData.initialX; var newY = dragData.initialY; if (dragData.extraWidth > 0) { newX = dragData.initialX - (dx * 100 / dragData.extraWidth); } if (dragData.extraHeight > 0) { newY = dragData.initialY - (dy * 100 / dragData.extraHeight); } newX = Math.max(0, Math.min(100, newX)); newY = Math.max(0, Math.min(100, newY)); $img.css('object-position', newX + '% ' + newY + '%'); } function onEnd(e) { $(document).off('mousemove touchmove', onMove); $(document).off('mouseup touchend', onEnd); // 드래그 종료 시 최종 object-position 값을 저장하여 다음 드래그의 초기값으로 사용 var finalObjPos = $img.css('object-position'); $img.data('initialObjPos', finalObjPos); } $(document).on('mousemove touchmove', onMove); $(document).on('mouseup touchend', onEnd); }); $('#editor').on('click', '.resizable', function (e) { e.stopPropagation(); // 이전 선택된 이미지 해제 $('.resizable').removeClass('selected'); $(this).addClass('selected'); // 선택된 이미지로 효과 동기화 selectImage($(this).find('img')); showImageToolbar($(this)); // 툴바 표시 // ▼ 추가: 이미지 링크 정보 확인 후 입력창에 반영 const $img = $(this).find('img'); const $anchor = $img.parent('a'); // 부모가 인지 검사 if ($anchor.length) { // 링크가 있으면 href, target 정보를 UI에 반영 const hrefVal = $anchor.attr('href') || ''; const targetVal = $anchor.attr('target') || '_parent'; $('#rb-image-link-inp').text(hrefVal); $('#rb-image-link-blanks').prop('checked', targetVal === '_blank'); } else { // 링크가 없으면 입력창 초기화 $('#rb-image-link-inp').text(''); $('#rb-image-link-blanks').prop('checked', false); } document.activeElement.blur(); }); $('#mini-image-link-del-btn').on('click', function () { // 선택된 .resizable 내부의 img const $selectedImg = $('.resizable.selected img'); if (!$selectedImg.length) { alert('이미지를 선택하세요.'); return; } $selectedImg.each(function () { let $img = $(this); let $parent = $img.parent(); if ($parent.is('a')) { // 태그 제거, 만 남김 $parent.replaceWith($img); } }); // 입력창 초기화 $('#rb-image-link-inp').text(''); $('#rb-image-link-blanks').prop('checked', false); }); $('#image-toolbar').on('click', function (e) { e.stopPropagation(); // 이벤트 전파 방지 }); // #image-toolbar 드래그(마우스 & 터치)로 이동 가능하게 하기 $('#image-toolbar #move_handle').on('mousedown touchstart', function(e) { e.preventDefault(); e.stopPropagation(); var $handle = $(this); var $toolbar = $handle.closest('#image-toolbar'); var $container = $('#editor-container'); var containerOffset = $container.offset(); // 드래그 시작 시의 좌표 (마우스/터치 구분) var startX = e.type === 'mousedown' ? e.clientX : e.touches[0].clientX; var startY = e.type === 'mousedown' ? e.clientY : e.touches[0].clientY; // 현재 툴바의 상대적 위치 (컨테이너 기준) var toolbarOffset = $toolbar.offset(); var initialLeft = toolbarOffset.left - containerOffset.left; var initialTop = toolbarOffset.top - containerOffset.top; function onMove(e) { var moveX = e.type === 'mousemove' ? e.clientX : e.touches[0].clientX; var moveY = e.type === 'mousemove' ? e.clientY : e.touches[0].clientY; var dx = moveX - startX; var dy = moveY - startY; // 새 위치 (컨테이너 기준) var newLeft = initialLeft + dx; var newTop = initialTop + dy; // 경계 체크 (컨테이너 내에 머물도록) var containerWidth = $container.width(); var containerHeight = $container.height(); var toolbarWidth = $toolbar.outerWidth(); var toolbarHeight = $toolbar.outerHeight(); newLeft = Math.max(0, Math.min(newLeft, containerWidth - toolbarWidth)); newTop = Math.max(0, Math.min(newTop, containerHeight - toolbarHeight)); $toolbar.css({ left: newLeft + 'px', top: newTop + 'px' }); // 이동한 위치 저장 (컨테이너 기준) savedToolbarPosition = { left: newLeft, top: newTop }; } function onEnd(e) { $(document).off('mousemove touchmove', onMove); $(document).off('mouseup touchend', onEnd); } $(document).on('mousemove touchmove', onMove); $(document).on('mouseup touchend', onEnd); }); /* 3. 이미지 리사이징 기능 (커스텀) */ function makeImageResizableWithObserver($resizable) { makeImageResizable($resizable); observeResizable($resizable); } function makeImageResizable($resizable) { $resizable.removeData('resizable'); $resizable.data('resizable', true); $resizable.css('position', 'relative'); let $handle = $resizable.find('.resize-handle'); if ($handle.length === 0) { $handle = $('
'); $resizable.append($handle); } else { $handle.off('mousedown touchstart'); } function startResize(e, isTouch) { e.preventDefault(); e.stopPropagation(); const $content = $resizable.find('img, iframe'); // ✅ 이미지 또는 iframe if ($content.length === 0) return; // ✅ 콘텐츠 없으면 종료 let startWidth = $resizable.width(); let startHeight = $resizable.height(); // ✅ 콘텐츠 크기 가져오기 (이미지: naturalWidth, iframe: getBoundingClientRect) let originalWidth = $content.is('img') ? $content[0].naturalWidth : $content[0].getBoundingClientRect().width; let originalHeight = $content.is('img') ? $content[0].naturalHeight : $content[0].getBoundingClientRect().height; let originalRatio = originalHeight / originalWidth; let startX = 0, startY = 0; let initialPinchDistance = null; let shiftKeyPressed = e.shiftKey; if (isTouch && e.touches.length >= 2) { initialPinchDistance = getPinchDistance(e); } else { startX = isTouch ? e.touches[0].clientX : e.clientX; startY = isTouch ? e.touches[0].clientY : e.clientY; } // ✅ `.url-preview-video` 내부일 경우 iframe 포인터 비활성화 if ($resizable.closest('.url-preview-video').length > 0) { $resizable.closest('.url-preview-video').find('iframe').css('pointer-events', 'none'); } function doResize(e) { e.preventDefault(); let newWidth = startWidth; let newHeight = startHeight; shiftKeyPressed = e.shiftKey; if (isTouch && e.touches.length >= 2) { let currentPinchDistance = getPinchDistance(e); if (initialPinchDistance && currentPinchDistance) { let scaleFactor = currentPinchDistance / initialPinchDistance; newWidth = startWidth * scaleFactor; newHeight = shiftKeyPressed ? newWidth * originalRatio : startHeight * scaleFactor; } if ($content.is('img')) { $content.css('object-fit', ''); // ✅ 이미지 비율 유지 } } else { const moveX = isTouch ? e.touches[0].clientX : e.clientX; const moveY = isTouch ? e.touches[0].clientY : e.clientY; const dx = moveX - startX; const dy = moveY - startY; if (shiftKeyPressed || $resizable.closest('.url-preview-video').length > 0) { // ✅ Shift 키가 눌렸거나 `.url-preview-video` 내부일 경우 원본 비율 유지 newWidth = startWidth + dx; newHeight = newWidth * originalRatio; if ($content.is('img')) { $content.css('object-fit', ''); // ✅ 이미지 비율 유지 } } else { // ✅ 자유 크기 조절 모드 (iframe 포함) newWidth = startWidth + dx; newHeight = startHeight + dy; if ($content.is('img')) { $content.css('object-fit', 'cover'); // ✅ 비율 유지 없음 } } } if (newWidth > 20 && newHeight > 20) { $resizable.css({ width: newWidth + 'px', height: newHeight + 'px' }); // ✅ Shift 키, 투핑거 터치, `.url-preview-video` 내부에서는 원본 비율 유지 if (shiftKeyPressed || (isTouch && e.touches.length >= 2) || $resizable.closest('.url-preview-video').length > 0) { newHeight = newWidth * originalRatio; $resizable.css({ height: newHeight + 'px' }); } } } function stopResize() { document.removeEventListener(isTouch ? 'touchmove' : 'mousemove', doResize); document.removeEventListener(isTouch ? 'touchend' : 'mouseup', stopResize); let finalWidth = $resizable.width(); let finalHeight = $resizable.height(); // ✅ 최종 크기 저장 $resizable.attr('data-original-width', finalWidth); $resizable.attr('data-original-height', finalHeight); $resizable.attr('data-ratio', finalHeight / finalWidth); // ✅ `.url-preview-video` 내부일 경우 iframe 포인터 이벤트 복구 if ($resizable.closest('.url-preview-video').length > 0) { $resizable.closest('.url-preview-video').find('iframe').css('pointer-events', ''); } } document.addEventListener(isTouch ? 'touchmove' : 'mousemove', doResize, { passive: false }); document.addEventListener(isTouch ? 'touchend' : 'mouseup', stopResize, { passive: false }); } const handleEl = $handle[0]; handleEl.addEventListener('mousedown', function(e) { startResize(e, false); }, { passive: false }); handleEl.addEventListener('touchstart', function(e) { startResize(e, true); }, { passive: false }); $resizable.on('touchstart', function(e) { if (e.touches.length >= 2 && $(e.target).closest('.resize-handle').length === 0) { startResize(e, true); } }); } // 헬퍼: 두 손가락 사이 거리를 계산하는 함수 function getPinchDistance(e) { if (e.touches.length >= 2) { var dx = e.touches[0].clientX - e.touches[1].clientX; var dy = e.touches[0].clientY - e.touches[1].clientY; return Math.sqrt(dx * dx + dy * dy); } return null; } /* 4. MutationObserver를 사용하여 'resizable' div의 클래스 변경 감지 */ const observer = new MutationObserver(function (mutationsList) { mutationsList.forEach(mutation => { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { const $target = $(mutation.target); if ($target.hasClass('selected')) { showImageToolbar($target); } else { $('#image-toolbar').fadeOut(0); selectedImage = null; } } }); }); // 모든 'resizable' div에 대해 MutationObserver 설정 function observeResizable($resizable) { observer.observe($resizable[0], { attributes: true }); } function showImageToolbar($resizable) { selectedImage = $resizable.find('img'); // 저장된 위치가 있으면 그대로 사용 if (savedToolbarPosition) { $('#image-toolbar').css({ top: savedToolbarPosition.top + 'px', left: savedToolbarPosition.left + 'px', }).fadeIn(0); } else { // 기존 로직에 따른 위치 계산 const imgOffset = $resizable.offset(); const containerOffset = $('body').offset(); const toolbarHeight = $('#image-toolbar').outerHeight(); const toolbarWidth = $('#image-toolbar').outerWidth(); const containerWidth = $('#editor-container').width(); const containerHeight = $('#editor-container').height(); let toolbarTop = imgOffset.top - containerOffset.top + 15; let toolbarLeft = selectedImage.width() + 50; if (toolbarTop + toolbarHeight > containerHeight) { toolbarTop = imgOffset.top - containerOffset.top - toolbarHeight; } if (toolbarLeft + toolbarWidth > containerWidth) { toolbarLeft = containerWidth - toolbarWidth; } if (toolbarLeft < 0) { toolbarLeft = 10; } if (toolbarTop < 0) { toolbarTop = 10; } $('#image-toolbar').css({ top: toolbarTop + 'px', left: toolbarLeft + 'px', }).fadeIn(0); } } $('#radius-slider').on('input', function () { if ($('.resizable.selected').length) { const radiusValue = $(this).val(); $('.resizable.selected img').css('border-radius', radiusValue + 'px'); } }); $('#remove-image-btn').click(function () { if (!selectedImage) return; if (currentMode === 'regular') { const $img = $(selectedImage); // 1) 가장 먼저 .resizable_wrap, .resizable를 찾고 let $target = $img.closest('.resizable_wrap, .resizable'); // 2) 없다면
태그가 있는지 확인 if (!$target.length) { $target = $img.closest('a'); } // 3) 그래도 없으면 자기 자신(img)만 제거 if ($target.length) { $target.remove(); } else { $img.remove(); } } else if (currentMode === 'canvas') { fabricCanvas.remove(selectedImage); } $('#image-toolbar').fadeOut(0); selectedImage = null; }); /* $('#remove-server-image-btn').click(function () { if (!selectedImage) return; let imageUrl = selectedImage.attr('src'); // 삭제 확인 메시지 if (!confirm("서버에서 이미지를 완전히 삭제합니다. 삭제된 이미지는 복구되지 않으며, 이미지를 감싸는 영역도 함께 제거됩니다.\n\n이미지를 서버에서 완전히 삭제 하시겠습니까?")) { return; } var g5_editor_url = "/plugin/editor/rb.editor"; // 올바른 경로 설정 // 서버에서 파일 삭제 요청 $.ajax({ url: g5_editor_url + "/php/rb.delete.php", // 삭제 처리 파일 type: "POST", data: { file: imageUrl }, // 파일 URL 전송 dataType: "json", success: function (response) { console.log("서버 응답 (파일 삭제):", response); if (response.success) { // ✅ 서버에서 파일 삭제 성공 시 `.resizable` 삭제 selectedImage.parent('.resizable').remove(); // 툴바 숨김 및 선택 이미지 초기화 $('#image-toolbar').fadeOut(0); selectedImage = null; } else { alert("이미지 삭제 실패: " + (response.error || "알 수 없는 오류")); } }, error: function (xhr, status, error) { console.error("파일 삭제 오류:", error); alert("파일 삭제 중 오류가 발생했습니다."); } }); }); */ $(document).click(function (e) { if (!$(e.target).closest('#image-toolbar').length && !$(e.target).closest('.resizable').length) { $('#image-toolbar').fadeOut(0); selectedImage = null; } }); // 커서를 박스 외부로 강제 이동하는 함수 function moveCursorOutsideResizable(range) { const resizable = $(range.startContainer).closest('.resizable'); if (resizable.length) { // 박스 바로 뒤로 커서를 이동 const parentNode = resizable[0].parentNode; const resizableIndex = Array.from(parentNode.childNodes).indexOf(resizable[0]); // 새로운 Range 설정 const newRange = document.createRange(); const selection = window.getSelection(); if (parentNode.childNodes[resizableIndex + 1]) { // 박스 뒤로 이동 newRange.setStart(parentNode.childNodes[resizableIndex + 1], 0); } else { // 박스가 마지막 노드라면 부모의 끝으로 이동 newRange.setStartAfter(resizable[0]); } newRange.collapse(true); // 선택된 Range 업데이트 selection.removeAllRanges(); selection.addRange(newRange); } } function wrapImagesWithResizable() { $('#editor img').not('.url-preview img').each(function () { var $img = $(this); var $resizable, $resizableWrap; // 이미 `.resizable_wrap`으로 감싸져 있는지 확인 if ($img.closest('.resizable_wrap').length) { $resizableWrap = $img.closest('.resizable_wrap'); $resizable = $resizableWrap.find('.resizable'); } else { // ✅ `.resizable_wrap` 생성 $resizableWrap = $('
'); // ✅ `.resizable` 생성 $resizable = $('
'); // ✅ `.resizable` 기본 스타일 적용 $resizable.css({ position: 'relative', userSelect: 'none', caretColor: 'transparent', pointerEvents: 'auto' }); // ✅ 크기 조절 핸들 추가 var $resizeHandle = $('
'); $resizable.append($img.clone()).append($resizeHandle); // ✅ 구조 변경: `.resizable_wrap` → `.resizable` → `` $resizableWrap.append($resizable); $img.replaceWith($resizableWrap); // ✅ 크기 조절 기능 활성화 makeImageResizableWithObserver($resizable); } // ✅ 현재 `.resizable`의 크기 저장 var currentWidth = $resizable.width(); var currentHeight = $resizable.height(); $resizable.attr('data-original-width', currentWidth); $resizable.attr('data-original-height', currentHeight); }); } $(document).ready(function () { /* 완전삭제를 사용하는 경우 함수교체 function initResizableElements() { $('.resizable').each(function () { var $this = $(this); var $img = $this.find('img'); // 이미지가 없거나, 이미지가 로드되지 않으면 .resizable 제거 if ($img.length === 0) { console.warn("이미지 없음 - .resizable 삭제", $this); $this.remove(); return; } // 이미지가 존재하는지 확인 (비동기 방식) checkImageExists($img.attr('src'), function(exists) { if (!exists) { console.warn("이미지 로드 실패 - .resizable 삭제", $this); $this.remove(); } }); observeResizable($this); makeImageResizableWithObserver($this); }); } // ✅ 이미지 존재 여부 확인 함수 (fetch 사용) function checkImageExists(imageUrl, callback) { fetch(imageUrl, { method: 'HEAD' }) .then(response => callback(response.ok)) .catch(() => callback(false)); } */ function initResizableElements() { $('.resizable').each(function () { var $this = $(this); observeResizable($this); makeImageResizableWithObserver($this); }); } // 최초 실행 (초기 로드된 .resizable 요소 감지) initResizableElements(); wrapImagesWithResizable(); // MutationObserver 설정 (동적 감지) const observer = new MutationObserver(() => { initResizableElements(); wrapImagesWithResizable(); }); // #editor 내부에서 변경 사항 감지 observer.observe(document.getElementById('editor'), { childList: true, subtree: true }); });