diff --git a/components/admin/AdminBlockEditor.vue b/components/admin/AdminBlockEditor.vue index 8dab6da..72b721f 100644 --- a/components/admin/AdminBlockEditor.vue +++ b/components/admin/AdminBlockEditor.vue @@ -132,9 +132,9 @@ const blockCommands = [ }, { type: 'embed', - label: '임베드', - description: 'YouTube 등 외부 링크 삽입', - keywords: ['embed', 'youtube', 'link', '임베드', '유튜브'] + label: 'HTML', + description: 'URL 또는 iframe HTML 삽입', + keywords: ['html', 'iframe', 'embed', 'youtube', 'link', '임베드', '유튜브'] }, { type: 'quote', @@ -2281,12 +2281,12 @@ defineExpose({
diff --git a/components/admin/AdminEditorBlockPanel.vue b/components/admin/AdminEditorBlockPanel.vue index 47f175c..3cca685 100644 --- a/components/admin/AdminEditorBlockPanel.vue +++ b/components/admin/AdminEditorBlockPanel.vue @@ -115,7 +115,7 @@ const panelTitle = computed(() => { } if (props.panel.kind === 'embed') { - return '임베드' + return 'HTML' } if (props.panel.kind === 'quote') { @@ -155,7 +155,7 @@ const panelMeta = computed(() => { } if (props.panel.kind === 'embed') { - return 'YouTube·X 등 URL' + return 'URL·iframe' } if (props.panel.kind === 'quote') { @@ -254,17 +254,16 @@ const onPanelFocusOut = (event) => {- YouTube·YouTube Shorts, X(트위터) 게시물 URL을 지원합니다. 그 외 URL은 링크 카드로 표시됩니다. + YouTube·YouTube Shorts, X(트위터) 게시물 URL과 외부 서비스 iframe 코드를 지원합니다. iframe은 src 등 허용된 속성만 사용합니다.
diff --git a/components/admin/AdminMarkdownEditor.vue b/components/admin/AdminMarkdownEditor.vue index 8e3db7f..63bcafa 100644 --- a/components/admin/AdminMarkdownEditor.vue +++ b/components/admin/AdminMarkdownEditor.vue @@ -1472,18 +1472,39 @@ const getSelectionState = () => { * @returns {void} */ const setTextareaSelection = (start, end = start, options = {}) => { - const previousScrollTop = options.preserveScroll ? textareaRef.value?.scrollTop : null + const scrollContainer = options.preserveScroll && import.meta.client + ? getNearestScrollContainer(editorRootRef.value) + : null + const previousScrollState = options.preserveScroll + ? { + textareaTop: textareaRef.value?.scrollTop, + containerTop: scrollContainer?.scrollTop, + windowX: import.meta.client ? window.scrollX : 0, + windowY: import.meta.client ? window.scrollY : 0 + } + : null + /** - * textarea 스크롤 위치를 복원한다. + * textarea와 에디터 스크롤 위치를 복원한다. * @param {HTMLTextAreaElement} textarea - 대상 textarea * @returns {void} */ const restoreScroll = (textarea) => { - if (!Number.isFinite(previousScrollTop)) { + if (!previousScrollState) { return } - textarea.scrollTop = previousScrollTop + if (Number.isFinite(previousScrollState.textareaTop)) { + textarea.scrollTop = previousScrollState.textareaTop + } + + if (scrollContainer && Number.isFinite(previousScrollState.containerTop)) { + scrollContainer.scrollTop = previousScrollState.containerTop + } + + if (import.meta.client && Number.isFinite(previousScrollState.windowY)) { + window.scrollTo(previousScrollState.windowX, previousScrollState.windowY) + } } nextTick(() => { @@ -2729,8 +2750,8 @@ const appendImagesToActiveGallery = (images) => { } /** - * 현재 임베드 URL을 수정한다. - * @param {string} url - 임베드 URL + * 현재 HTML 블록 값을 수정한다. + * @param {string} url - HTML 블록 값 * @returns {void} */ const updateActiveEmbedUrl = (url) => { @@ -2748,6 +2769,11 @@ const updateActiveEmbedUrl = (url) => { return } + if (trimmed.includes('\n') || /^