var urlRegex = /(https?:\/\/[^\s]+)/g; var imageRegex = /\.(jpeg|jpg|gif|png|webp|svg)$/i; var pendingRequests = {}; function extractVideoId(url) { let videoId = null; let timeParam = ''; if (url.includes('youtube.com') || url.includes('youtu.be')) { let match = url.match(/(?:v=|youtu\.be\/|embed\/|shorts\/)([\w-]+)/); videoId = match ? match[1] : null; let timeMatch = url.match(/[?&]t=(\d+)/); timeParam = timeMatch ? `?start=${timeMatch[1]}` : ''; return videoId ? `https://www.youtube.com/embed/${videoId}${timeParam}` : null; } if (url.includes('vimeo.com')) { let match = url.match(/vimeo\.com\/(\d+)/); videoId = match ? match[1] : null; return videoId ? `https://player.vimeo.com/video/${videoId}` : null; } return null; } function fetchMetadataAndDisplay(url, range) { //
 내부에서 실행되지 않도록 차단
        if ($(range.commonAncestorContainer).closest('pre').length > 0) {
            console.warn("🚫 
 내부에서는 미디어 또는 메타데이터를 삽입할 수 없습니다.");
            return;
        }

        let embedUrl = extractVideoId(url);

        if ($(range.commonAncestorContainer).closest('table, td, th').length) {
            return;
        }

        if (embedUrl) {
            displayVideoPreview(url, embedUrl, range);
            return;
        }

        $.ajax({
            url: g5Config.g5_editor_url + '/php/rb.metadata.php',
            method: 'GET',
            data: { url: url },
            dataType: 'json',
            success: function (response) {
                if (response.error) {
                    return;
                }

                var meta = response.meta;
                var title = response.title || '제목 없음';
                var description = meta['description'] || meta['og:description'] || '';
                var image = meta['og:image'] || '';

                var wrapper = $('
'); // 메타데이터 감싸는 div var html = $('
'); var imageWrapper = $('
  • '); // 이미지 감싸는 요소 var metaData = $(''); function validateImageUrl(imageUrl, callback) { var img = new Image(); img.src = imageUrl; img.onload = function () { callback(true); // 이미지 로드 성공 }; img.onerror = function () { callback(false); // 이미지 로드 실패 }; } if (image) { var imageProxyUrl = g5Config.g5_editor_url + '/php/rb.image_proxy.php?url=' + encodeURIComponent(image); var imageElement = $('Preview Image'); validateImageUrl(imageProxyUrl, function (isValid) { if (isValid) { imageWrapper.append(imageElement); html.append(imageWrapper); } else { imageWrapper.remove(); // 엑박 방지: 이미지가 없으면 제거 metaData.css("padding-left", "0"); // 자동으로 padding-left 제거 } }); imageElement.on('error', function () { imageWrapper.remove(); // 이미지가 깨지면 자동 제거 metaData.css("padding-left", "0"); // padding-left: 0 적용 }); } else { metaData.css("padding-left", "0"); // 이미지가 없으면 자동 적용 } metaData.append('

    ' + title + '

    '); metaData.append('

    ' + description + '

    '); metaData.append('' + url + ''); html.append(metaData); wrapper.append(html); insertMediaAfterUrl(wrapper, range); } }); } function displayVideoPreview(url, embedUrl, range) { //
     내부에서는 동영상 미리보기를 삽입하지 않도록 제한
            if ($(range.commonAncestorContainer).closest('pre').length > 0) {
                //console.warn("
     내부에서는 동영상 미리보기를 삽입할 수 없습니다.");
                return;
            }
    
            if ($(range.commonAncestorContainer).closest('table, td, th').length) {
                return;
            }
    
            var wrapper = $('
    '); // ✅ 동영상 감싸는 div var html = $('
    '); html.append('
    '); wrapper.append(html); // 감싸는 div 추가 insertMediaAfterUrl(wrapper, range); } function insertMediaAfterUrl(node, range) { range.collapse(false); range.insertNode(node[0]); range.collapse(false); var selection = window.getSelection(); selection.removeAllRanges(); var newRange = document.createRange(); newRange.setStartAfter(node[0]); newRange.collapse(true); selection.addRange(newRange); // 미디어 노드 내의 .resizable 요소에 대해 현재 크기를 data 속성으로 업데이트 node.find('.resizable').each(function(){ var $this = $(this); var currentWidth = $this.width(); var currentHeight = $this.height(); $this.attr('data-original-width', currentWidth); $this.attr('data-original-height', currentHeight); // 필요시 data-ratio도 갱신 (세로/가로) $this.attr('data-ratio', currentHeight / currentWidth); }); } function insertImageDirectly(url, range) { if ($(range.commonAncestorContainer).closest('table, td, th').length) { return; } var wrapper = $('
    '); // ✅ 이미지 감싸는 div var html = $('
    '); var img = $('Embedded Image'); img.on('load', function () { // ✅ 이미지 원본 크기 저장 var imgWidth = this.naturalWidth; var imgHeight = this.naturalHeight; var ratio = imgHeight / imgWidth; const editorWidth = $('#editor').width(); // 에디터의 가로 크기 // ✅ 에디터보다 크면 width: 100%, 작으면 원본 크기 유지 if (imgWidth > editorWidth) { html.css('width', '100%'); } else { html.css('width', imgWidth + 'px'); } // ✅ 원본 크기 저장 (비율 유지용) html.attr('data-original-width', imgWidth); html.attr('data-original-height', imgHeight); html.attr('data-ratio', ratio); // ✅ 높이 설정 (현재 width에 따라 비율 유지) html.css('height', (html.width() * ratio) + 'px'); // ✅ 창 크기 변경 시 높이 자동 조정 function updateHeight() { var currentWidth = html.width(); var originalWidth = parseFloat(html.attr('data-original-width')) || currentWidth; var originalHeight = parseFloat(html.attr('data-original-height')) || (currentWidth * ratio); var originalRatio = parseFloat(html.attr('data-ratio')) || (originalHeight / originalWidth); html.css('height', (currentWidth * originalRatio) + 'px'); } $(window).on('resize', updateHeight); // ✅ 크기 조절 기능 적용 (비율 유지) makeImageResizableWithObserver(html); }); html.append(img); html.append(''); wrapper.append(html); insertMediaAfterUrl(wrapper, range); } $('#editor').on('click', '.delete-preview', function (e) { e.preventDefault(); e.stopPropagation(); // 부모로의 이벤트 버블링 방지 $(this).closest('.url-preview-img, .url-preview-meta, .url-preview-video').remove(); $('#image-toolbar').fadeOut(0); selectedImage = null; });