Files
2025-07-02 14:13:16 +09:00

895 lines
31 KiB
JavaScript

/* ========== 전역 변수 및 기본 설정 ========== */
// 이미지별 효과 상태를 관리하는 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('<div class="resize-handle"></div>');
// 새로 추가된 핸들에 대해 커스텀 리사이징 이벤트를 다시 바인딩
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'); // 부모가 <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')) {
// <a> 태그 제거, <img>만 남김
$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 = $('<div class="resize-handle"></div>');
$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) 없다면 <a> 태그가 있는지 확인
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 = $('<div class="resizable_wrap"></div>');
// ✅ `.resizable` 생성
$resizable = $('<div class="resizable" contenteditable="false" draggable="true"></div>');
// ✅ `.resizable` 기본 스타일 적용
$resizable.css({
position: 'relative',
userSelect: 'none',
caretColor: 'transparent',
pointerEvents: 'auto'
});
// ✅ 크기 조절 핸들 추가
var $resizeHandle = $('<div class="resize-handle"></div>');
$resizable.append($img.clone()).append($resizeHandle);
// ✅ 구조 변경: `.resizable_wrap` → `.resizable` → `<img>`
$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 });
});