블록 패널 액션 적용 분리
This commit is contained in:
@@ -37,21 +37,6 @@ import {
|
||||
resolveImageUploadInsertTarget,
|
||||
resolveUploadFetchErrorMessage
|
||||
} from '../../lib/markdown-editor-media.js'
|
||||
import {
|
||||
createCalloutOptionsReplacementPlan,
|
||||
createCodeOptionsReplacementPlan,
|
||||
createEmbedUrlReplacementPlan,
|
||||
createGalleryImageMovePlan,
|
||||
createGalleryImagesAppendPlan,
|
||||
createLiveInlineColorValue,
|
||||
createMediaImageRemovePlan,
|
||||
createMediaImagesReplacementPlan,
|
||||
createMediaImageUpdatePlan,
|
||||
createMediaUseAltPatch,
|
||||
createQuoteBackgroundValue,
|
||||
createSourceInlineColorValue,
|
||||
createToggleOptionsReplacementPlan
|
||||
} from '../../lib/markdown-editor-block-options.js'
|
||||
import {
|
||||
createPreviewAppendParagraphValue,
|
||||
createPreviewDeleteLineValue,
|
||||
@@ -91,6 +76,7 @@ import { useAdminMarkdownFocusBridge } from '../../composables/useAdminMarkdownF
|
||||
import { useAdminMarkdownPreviewFocus } from '../../composables/useAdminMarkdownPreviewFocus.js'
|
||||
import { useAdminMarkdownTextareaFocus } from '../../composables/useAdminMarkdownTextareaFocus.js'
|
||||
import { useAdminMarkdownBlockPanel } from '../../composables/useAdminMarkdownBlockPanel.js'
|
||||
import { useAdminMarkdownBlockActions } from '../../composables/useAdminMarkdownBlockActions.js'
|
||||
import {
|
||||
buildDefaultUploadSizeLimits,
|
||||
DEFAULT_UPLOAD_MAX_FILE_COUNT,
|
||||
@@ -1181,52 +1167,6 @@ const resolveCurrentFencedBlockEndLine = (startLine, fallbackEndLine) => {
|
||||
return fallbackEndLine
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 미디어 블록을 이미지 목록 기준으로 다시 작성한다.
|
||||
* @param {Array<{ alt: string, url: string, width?: string }>} images - 이미지 목록
|
||||
* @returns {void}
|
||||
*/
|
||||
const replaceActiveMediaImages = (images) => {
|
||||
applyLineReplacementPlan(
|
||||
createMediaImagesReplacementPlan(activeMediaBlock.value, images),
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 미디어 블록의 특정 이미지를 수정한다.
|
||||
* @param {number} imageIndex - 이미지 인덱스
|
||||
* @param {Partial<{ alt: string, url: string, width: string, pixelWidth: number|null }>} patch - 변경 값
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveMediaImage = (imageIndex, patch) => {
|
||||
const plan = createMediaImageUpdatePlan(activeMediaBlock.value, imageIndex, patch)
|
||||
|
||||
if (!plan) {
|
||||
return
|
||||
}
|
||||
|
||||
ensureBlockPanelEngaged()
|
||||
applyLineReplacementPlan(plan, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 미디어 이미지의 파일명 캡션 사용 여부를 바꾼다.
|
||||
* @param {number} imageIndex - 이미지 인덱스
|
||||
* @param {boolean} enabled - 파일명 캡션 사용 여부
|
||||
* @returns {void}
|
||||
*/
|
||||
const setActiveMediaUseAlt = (imageIndex, enabled) => {
|
||||
const image = activeMediaBlock.value?.images[imageIndex]
|
||||
const patch = createMediaUseAltPatch(image, enabled)
|
||||
|
||||
if (!patch) {
|
||||
return
|
||||
}
|
||||
|
||||
updateActiveMediaImage(imageIndex, patch)
|
||||
}
|
||||
|
||||
/**
|
||||
* 미리보기에서 갤러리 순서 변경
|
||||
* @param {{ startLine: number, endLine: number, images: Array<Object> }} payload - 갤러리 줄 범위·이미지 목록
|
||||
@@ -1863,188 +1803,6 @@ const onPreviewMergeWithPreviousLine = ({ lineIndex, mergedLine }) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 갤러리 이미지 순서를 바꾼다.
|
||||
* @param {number} imageIndex - 이동할 이미지 인덱스
|
||||
* @param {-1|1} direction - 이동 방향
|
||||
* @returns {void}
|
||||
*/
|
||||
const moveActiveGalleryImage = (imageIndex, direction) => {
|
||||
const plan = createGalleryImageMovePlan(activeMediaBlock.value, imageIndex, direction)
|
||||
|
||||
if (!plan) {
|
||||
return
|
||||
}
|
||||
|
||||
applyLineReplacementPlan(plan, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 미디어 블록에서 이미지를 삭제한다.
|
||||
* @param {number} imageIndex - 삭제할 이미지 인덱스
|
||||
* @returns {void}
|
||||
*/
|
||||
const removeActiveMediaImage = (imageIndex) => {
|
||||
applyLineReplacementPlan(
|
||||
createMediaImageRemovePlan(activeMediaBlock.value, imageIndex),
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 갤러리에 이미지를 추가한다.
|
||||
* @param {Array<{ url: string, alt?: string, width?: string }>} images - 추가 이미지 목록
|
||||
* @returns {void}
|
||||
*/
|
||||
const appendImagesToActiveGallery = (images) => {
|
||||
const plan = createGalleryImagesAppendPlan(activeMediaBlock.value, images)
|
||||
|
||||
if (!plan) {
|
||||
insertGallery(images)
|
||||
return
|
||||
}
|
||||
|
||||
applyLineReplacementPlan(plan, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 링크·iframe·HTML 블록 값을 수정한다.
|
||||
* @param {string} url - 블록 값
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveEmbedUrl = (url) => {
|
||||
const plan = createEmbedUrlReplacementPlan(activeBlockContext.value, url)
|
||||
|
||||
if (!plan) {
|
||||
return
|
||||
}
|
||||
|
||||
ensureBlockPanelEngaged()
|
||||
applyLineReplacementPlan(plan, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 인용 블록 배경색을 수정한다.
|
||||
* @param {string} background - 배경색 옵션
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveQuoteBackground = (background) => {
|
||||
const nextValue = createQuoteBackgroundValue({
|
||||
value: markdownValue.value,
|
||||
block: activeBlockContext.value,
|
||||
background
|
||||
})
|
||||
|
||||
if (nextValue === null) {
|
||||
return
|
||||
}
|
||||
|
||||
ensureBlockPanelEngaged()
|
||||
markdownValue.value = nextValue
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 콜아웃 블록 옵션을 수정한다.
|
||||
* @param {Partial<{ calloutEmojiEnabled: boolean, calloutEmoji: string, calloutBackground: string, title: string }>} patch - 변경 옵션
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveCalloutOptions = (patch = {}) => {
|
||||
const plan = createCalloutOptionsReplacementPlan(activeBlockContext.value, patch)
|
||||
|
||||
if (!plan) {
|
||||
return
|
||||
}
|
||||
|
||||
ensureBlockPanelEngaged()
|
||||
applyLineReplacementPlan(plan, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 코드 블록 옵션을 수정한다.
|
||||
* @param {Partial<{ language: string, showLineNumbers: boolean }>} patch - 변경 옵션
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveCodeOptions = (patch = {}) => {
|
||||
const plan = createCodeOptionsReplacementPlan(activeBlockContext.value, patch)
|
||||
|
||||
if (!plan) {
|
||||
return
|
||||
}
|
||||
|
||||
ensureBlockPanelEngaged()
|
||||
applyLineReplacementPlan(plan, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 토글 블록 옵션을 수정한다.
|
||||
* @param {Partial<{ defaultOpen: boolean }>} patch - 변경 옵션
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveToggleOptions = (patch = {}) => {
|
||||
const plan = createToggleOptionsReplacementPlan(activeBlockContext.value, patch)
|
||||
|
||||
if (!plan) {
|
||||
return
|
||||
}
|
||||
|
||||
ensureBlockPanelEngaged()
|
||||
applyLineReplacementPlan(plan, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 선택된 텍스트에 인라인 색상을 적용한다.
|
||||
* @param {string} color - 색상 이름
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveInlineColor = (color) => {
|
||||
const context = inlineColorSelectionContext.value
|
||||
|
||||
if (!context || context.kind !== 'inline-color') {
|
||||
return
|
||||
}
|
||||
|
||||
ensureBlockPanelEngaged()
|
||||
|
||||
const sourceResult = createSourceInlineColorValue({
|
||||
value: markdownValue.value,
|
||||
context,
|
||||
color
|
||||
})
|
||||
|
||||
if (sourceResult) {
|
||||
markdownValue.value = sourceResult.value
|
||||
inlineColorSelectionContext.value = null
|
||||
setTextareaSelection(sourceResult.selectionStart, sourceResult.selectionEnd, { preserveScroll: true })
|
||||
return
|
||||
}
|
||||
|
||||
const liveResult = createLiveInlineColorValue({
|
||||
value: markdownValue.value,
|
||||
context,
|
||||
color
|
||||
})
|
||||
|
||||
if (!liveResult) {
|
||||
return
|
||||
}
|
||||
|
||||
markdownValue.value = liveResult.value
|
||||
inlineColorSelectionContext.value = null
|
||||
requestPreviewFocus({
|
||||
line: liveResult.focusLine,
|
||||
offset: liveResult.focusOffset
|
||||
})
|
||||
nextTick(() => {
|
||||
focusPreviewEditableAtLine(
|
||||
liveResult.focusLine,
|
||||
0,
|
||||
'auto',
|
||||
liveResult.focusOffset,
|
||||
'nearest'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 마크다운을 삽입한다.
|
||||
* @param {{ url: string, alt?: string, width?: string }} image - 이미지 정보
|
||||
@@ -2067,6 +1825,31 @@ const insertGallery = (images) => {
|
||||
insertBlockSnippet([':::gallery', ...images.map(createImageMarkdown), ':::'].join('\n'))
|
||||
}
|
||||
|
||||
const {
|
||||
appendImagesToActiveGallery,
|
||||
moveActiveGalleryImage,
|
||||
removeActiveMediaImage,
|
||||
setActiveMediaUseAlt,
|
||||
updateActiveCalloutOptions,
|
||||
updateActiveCodeOptions,
|
||||
updateActiveEmbedUrl,
|
||||
updateActiveInlineColor,
|
||||
updateActiveMediaImage,
|
||||
updateActiveQuoteBackground,
|
||||
updateActiveToggleOptions
|
||||
} = useAdminMarkdownBlockActions({
|
||||
value: markdownValue,
|
||||
activeMediaBlock,
|
||||
activeBlockContext,
|
||||
inlineColorSelectionContext,
|
||||
applyLineReplacementPlan,
|
||||
ensureBlockPanelEngaged,
|
||||
insertGallery,
|
||||
setTextareaSelection,
|
||||
requestPreviewFocus,
|
||||
focusPreviewEditableAtLine
|
||||
})
|
||||
|
||||
/**
|
||||
* 미디어 선택 창을 연다.
|
||||
* @param {'image'|'gallery'|'active-gallery'} target - 삽입 대상
|
||||
|
||||
264
composables/useAdminMarkdownBlockActions.js
Normal file
264
composables/useAdminMarkdownBlockActions.js
Normal file
@@ -0,0 +1,264 @@
|
||||
import { nextTick } from 'vue'
|
||||
import {
|
||||
createCalloutOptionsReplacementPlan,
|
||||
createCodeOptionsReplacementPlan,
|
||||
createEmbedUrlReplacementPlan,
|
||||
createGalleryImageMovePlan,
|
||||
createGalleryImagesAppendPlan,
|
||||
createLiveInlineColorValue,
|
||||
createMediaImageRemovePlan,
|
||||
createMediaImagesReplacementPlan,
|
||||
createMediaImageUpdatePlan,
|
||||
createMediaUseAltPatch,
|
||||
createQuoteBackgroundValue,
|
||||
createSourceInlineColorValue,
|
||||
createToggleOptionsReplacementPlan
|
||||
} from '../lib/markdown-editor-block-options.js'
|
||||
|
||||
/**
|
||||
* 관리자 Markdown 블록 패널의 옵션 적용 함수를 만든다.
|
||||
* @param {Object} options - 블록 액션 옵션
|
||||
* @param {import('vue').Ref<string>} options.value - Markdown 본문 ref
|
||||
* @param {import('vue').Ref<Object|null>} options.activeMediaBlock - 현재 미디어 블록 ref
|
||||
* @param {import('vue').Ref<Object|null>} options.activeBlockContext - 현재 활성 블록 ref
|
||||
* @param {import('vue').Ref<Object|null>} options.inlineColorSelectionContext - 인라인 색상 선택 컨텍스트 ref
|
||||
* @param {(plan: Object|null, focusEditor?: boolean) => boolean} options.applyLineReplacementPlan - 줄 교체 계획 적용 함수
|
||||
* @param {() => void} options.ensureBlockPanelEngaged - 블록 패널 유지 함수
|
||||
* @param {(images: Array<Object>) => void} options.insertGallery - 갤러리 삽입 함수
|
||||
* @param {(start: number, end?: number, options?: Object) => void} options.setTextareaSelection - textarea 선택 범위 설정 함수
|
||||
* @param {(focus: Object) => void} options.requestPreviewFocus - 미리보기 포커스 예약 함수
|
||||
* @param {(line: number, column?: number, align?: string, offset?: number, block?: string) => void} options.focusPreviewEditableAtLine - 미리보기 줄 포커스 함수
|
||||
* @returns {Object} 블록 패널 옵션 적용 함수
|
||||
*/
|
||||
export const useAdminMarkdownBlockActions = (options) => {
|
||||
/**
|
||||
* 줄 교체 계획을 블록 패널 유지와 함께 적용한다.
|
||||
* @param {Object|null} plan - 줄 교체 계획
|
||||
* @param {boolean} [engage=true] - 패널 유지 여부
|
||||
* @returns {boolean} 적용 여부
|
||||
*/
|
||||
const applyPanelLineReplacementPlan = (plan, engage = true) => {
|
||||
if (!plan) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (engage) {
|
||||
options.ensureBlockPanelEngaged()
|
||||
}
|
||||
|
||||
return options.applyLineReplacementPlan(plan, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 미디어 블록을 이미지 목록 기준으로 다시 작성한다.
|
||||
* @param {Array<{ alt: string, url: string, width?: string }>} images - 이미지 목록
|
||||
* @returns {void}
|
||||
*/
|
||||
const replaceActiveMediaImages = (images) => {
|
||||
options.applyLineReplacementPlan(
|
||||
createMediaImagesReplacementPlan(options.activeMediaBlock.value, images),
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 미디어 블록의 특정 이미지를 수정한다.
|
||||
* @param {number} imageIndex - 이미지 인덱스
|
||||
* @param {Partial<{ alt: string, url: string, width: string, pixelWidth: number|null }>} patch - 변경 값
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveMediaImage = (imageIndex, patch) => {
|
||||
applyPanelLineReplacementPlan(
|
||||
createMediaImageUpdatePlan(options.activeMediaBlock.value, imageIndex, patch)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 미디어 이미지의 파일명 캡션 사용 여부를 바꾼다.
|
||||
* @param {number} imageIndex - 이미지 인덱스
|
||||
* @param {boolean} enabled - 파일명 캡션 사용 여부
|
||||
* @returns {void}
|
||||
*/
|
||||
const setActiveMediaUseAlt = (imageIndex, enabled) => {
|
||||
const image = options.activeMediaBlock.value?.images[imageIndex]
|
||||
const patch = createMediaUseAltPatch(image, enabled)
|
||||
|
||||
if (patch) {
|
||||
updateActiveMediaImage(imageIndex, patch)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 갤러리 이미지 순서를 바꾼다.
|
||||
* @param {number} imageIndex - 이동할 이미지 인덱스
|
||||
* @param {-1|1} direction - 이동 방향
|
||||
* @returns {void}
|
||||
*/
|
||||
const moveActiveGalleryImage = (imageIndex, direction) => {
|
||||
applyPanelLineReplacementPlan(
|
||||
createGalleryImageMovePlan(options.activeMediaBlock.value, imageIndex, direction),
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 미디어 블록에서 이미지를 삭제한다.
|
||||
* @param {number} imageIndex - 삭제할 이미지 인덱스
|
||||
* @returns {void}
|
||||
*/
|
||||
const removeActiveMediaImage = (imageIndex) => {
|
||||
applyPanelLineReplacementPlan(
|
||||
createMediaImageRemovePlan(options.activeMediaBlock.value, imageIndex),
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 갤러리에 이미지를 추가한다.
|
||||
* @param {Array<{ url: string, alt?: string, width?: string }>} images - 추가 이미지 목록
|
||||
* @returns {void}
|
||||
*/
|
||||
const appendImagesToActiveGallery = (images) => {
|
||||
const plan = createGalleryImagesAppendPlan(options.activeMediaBlock.value, images)
|
||||
|
||||
if (!plan) {
|
||||
options.insertGallery(images)
|
||||
return
|
||||
}
|
||||
|
||||
options.applyLineReplacementPlan(plan, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 링크·iframe·HTML 블록 값을 수정한다.
|
||||
* @param {string} url - 블록 값
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveEmbedUrl = (url) => {
|
||||
applyPanelLineReplacementPlan(
|
||||
createEmbedUrlReplacementPlan(options.activeBlockContext.value, url)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 인용 블록 배경색을 수정한다.
|
||||
* @param {string} background - 배경색 옵션
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveQuoteBackground = (background) => {
|
||||
const nextValue = createQuoteBackgroundValue({
|
||||
value: options.value.value,
|
||||
block: options.activeBlockContext.value,
|
||||
background
|
||||
})
|
||||
|
||||
if (nextValue === null) {
|
||||
return
|
||||
}
|
||||
|
||||
options.ensureBlockPanelEngaged()
|
||||
options.value.value = nextValue
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 콜아웃 블록 옵션을 수정한다.
|
||||
* @param {Partial<{ calloutEmojiEnabled: boolean, calloutEmoji: string, calloutBackground: string, title: string }>} patch - 변경 옵션
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveCalloutOptions = (patch = {}) => {
|
||||
applyPanelLineReplacementPlan(
|
||||
createCalloutOptionsReplacementPlan(options.activeBlockContext.value, patch)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 코드 블록 옵션을 수정한다.
|
||||
* @param {Partial<{ language: string, showLineNumbers: boolean }>} patch - 변경 옵션
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveCodeOptions = (patch = {}) => {
|
||||
applyPanelLineReplacementPlan(
|
||||
createCodeOptionsReplacementPlan(options.activeBlockContext.value, patch)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 토글 블록 옵션을 수정한다.
|
||||
* @param {Partial<{ defaultOpen: boolean }>} patch - 변경 옵션
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveToggleOptions = (patch = {}) => {
|
||||
applyPanelLineReplacementPlan(
|
||||
createToggleOptionsReplacementPlan(options.activeBlockContext.value, patch)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 선택된 텍스트에 인라인 색상을 적용한다.
|
||||
* @param {string} color - 색상 이름
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateActiveInlineColor = (color) => {
|
||||
const context = options.inlineColorSelectionContext.value
|
||||
|
||||
if (!context || context.kind !== 'inline-color') {
|
||||
return
|
||||
}
|
||||
|
||||
options.ensureBlockPanelEngaged()
|
||||
|
||||
const sourceResult = createSourceInlineColorValue({
|
||||
value: options.value.value,
|
||||
context,
|
||||
color
|
||||
})
|
||||
|
||||
if (sourceResult) {
|
||||
options.value.value = sourceResult.value
|
||||
options.inlineColorSelectionContext.value = null
|
||||
options.setTextareaSelection(sourceResult.selectionStart, sourceResult.selectionEnd, { preserveScroll: true })
|
||||
return
|
||||
}
|
||||
|
||||
const liveResult = createLiveInlineColorValue({
|
||||
value: options.value.value,
|
||||
context,
|
||||
color
|
||||
})
|
||||
|
||||
if (!liveResult) {
|
||||
return
|
||||
}
|
||||
|
||||
options.value.value = liveResult.value
|
||||
options.inlineColorSelectionContext.value = null
|
||||
options.requestPreviewFocus({
|
||||
line: liveResult.focusLine,
|
||||
offset: liveResult.focusOffset
|
||||
})
|
||||
nextTick(() => {
|
||||
options.focusPreviewEditableAtLine(
|
||||
liveResult.focusLine,
|
||||
0,
|
||||
'auto',
|
||||
liveResult.focusOffset,
|
||||
'nearest'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
appendImagesToActiveGallery,
|
||||
moveActiveGalleryImage,
|
||||
removeActiveMediaImage,
|
||||
replaceActiveMediaImages,
|
||||
setActiveMediaUseAlt,
|
||||
updateActiveCalloutOptions,
|
||||
updateActiveCodeOptions,
|
||||
updateActiveEmbedUrl,
|
||||
updateActiveInlineColor,
|
||||
updateActiveMediaImage,
|
||||
updateActiveQuoteBackground,
|
||||
updateActiveToggleOptions
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
## 2026-07-29 v1.11.120 — 블록 패널 옵션 적용 실행을 composable로 분리한다
|
||||
|
||||
관리자 Markdown 에디터의 오른쪽 블록 패널은 이미 옵션 문자열 계산을 helper로 분리했지만, 계산 결과를 현재 문서에 반영하고 패널 포커스 유지·textarea 선택 복원·라이브 미리보기 포커스 예약을 실행하는 연결 함수가 여전히 컴포넌트에 모여 있었다. 이 실행 레이어를 컴포넌트 안에 두면 패널 UI 이벤트가 늘어날수록 에디터 본체가 다시 커진다. 따라서 `useAdminMarkdownBlockActions`가 활성 블록 ref와 실행 콜백을 주입받아 이미지·갤러리·임베드·콜아웃·코드·토글·인라인 색상 적용을 담당하고, 에디터는 DOM 참조와 본문 삽입 함수만 제공한다.
|
||||
|
||||
## 2026-07-29 v1.11.119 — 라이브 미리보기 블록 이벤트 계산을 helper로 분리한다
|
||||
|
||||
관리자 Markdown 에디터의 라이브 미리보기 이벤트는 갤러리 순서 변경, 단일 이미지 병합, 갤러리 내부 삽입, 이미지 분리, 문단 추가·삭제·병합처럼 Markdown 줄 배열을 직접 재구성한다. 이 계산이 컴포넌트에 남으면 실제 DOM 포커스 실행과 값 변경 규칙이 계속 섞인다. 따라서 `markdown-editor-preview-blocks` helper가 이벤트 payload와 현재 Markdown 값으로 다음 Markdown 값을 계산하고, 에디터는 계산 결과를 반영하는 연결점만 맡는다.
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
| composables/useAdminMarkdownPreviewFocus.js | 관리자 Markdown 에디터 라이브 미리보기 표면 DOM 접근, 현재 source line 계산, 상단 스크롤, 전체 선택, 줄 포커스 실행 관리 |
|
||||
| composables/useAdminMarkdownTextareaFocus.js | 관리자 Markdown 에디터 textarea 높이·거터 줄 높이·선택 기억·커서 줄 갱신·지정 줄 포커스·스크롤 실행 관리 |
|
||||
| composables/useAdminMarkdownBlockPanel.js | 관리자 Markdown 에디터 블록 설정 패널 표시 상태·포커스 유지·IME 조합 중 직전 컨텍스트 유지·인라인 색상 패널 우선순위 관리 |
|
||||
| composables/useAdminMarkdownBlockActions.js | 관리자 Markdown 에디터 오른쪽 블록 패널의 이미지·갤러리·임베드·인용·콜아웃·코드·토글·인라인 색상 옵션 적용 실행 관리 |
|
||||
| composables/useAdminMarkdownMediaPicker.js | 관리자 Markdown 에디터 미디어 선택 모달 열기·닫기·탭·검색어·페이지·선택 상태·표시 계산, 서버 미디어 목록 조회, 업로드 한도 검증·전송, 업로드 후 라이브러리 탭 전환·선택 상태 갱신 관리 |
|
||||
| composables/useContentMarkdownBlocks.js | 공개 Markdown 본문 파싱·제목 ID·인아티클 광고 렌더 블록·하단 삽입 줄·라이트박스 대상 목록 계산 |
|
||||
| composables/useContentMarkdownLightbox.js | 공개 Markdown 본문 단일 이미지·갤러리 통합 라이트박스 상태·좌우 이동·ESC 닫기·키보드 이벤트 연결 관리 |
|
||||
@@ -150,6 +151,7 @@
|
||||
| tests/admin-post-settings-sidebar-events.test.js | 관리자 글쓰기 설정 사이드바가 블록 패널의 다중 인자 이벤트를 그대로 전달하는지 확인하는 회귀 테스트 |
|
||||
| tests/markdown-editor-block-options.test.js | 관리자 Markdown 에디터 블록 옵션 줄 교체 계획과 인라인 색상 본문 변경 helper 단위 테스트 |
|
||||
| tests/markdown-editor-preview-blocks.test.js | 관리자 Markdown 에디터 라이브 미리보기 갤러리·이미지·문단 이벤트 값 변경 helper 단위 테스트 |
|
||||
| tests/useAdminMarkdownBlockActions.test.js | 관리자 Markdown 에디터 블록 패널 옵션 적용 composable 단위 테스트 |
|
||||
| tests/markdown-editor-media.test.js | 관리자 Markdown 에디터 미디어 선택 helper의 종류·선택 가능 여부·미리보기·선택 상태·삽입 계획·업로드 정규화·블록 생성 단위 회귀 테스트 |
|
||||
| tests/markdown-slash-commands.test.js | 관리자 슬래시 명령 파싱·입력값 기준 명령 해석·미디어 action·모달 종류·적용 계획·제외 줄 Set 갱신·하이라이트 인덱스 보정·방향키 순환·키보드 동작 분류·후보 개수 기준 키보드 가로채기·포커스 대상·textarea 선택 위치·원문 줄 교체 삽입 단위 회귀 테스트 |
|
||||
| tests/markdown-slash-menu-scroll.test.js | 관리자 슬래시 메뉴 선택 항목 scrollTop 보정 단위 테스트 |
|
||||
@@ -268,7 +270,7 @@
|
||||
| components/admin/AdminMarkdownWriteSurface.vue | 관리자 Markdown 에디터 소스 작성 표면 UI(textarea, 라인 번호 gutter, 업로드 상태 배지, DOM 참조 노출) |
|
||||
| components/admin/AdminPostSettingsSidebar.vue | 관리자 게시물 작성/수정 우측 설정 사이드바(Post URL·상태·발행 시각·요약·태그·대표 이미지 표시·추천 글·검색엔진 노출·본문 통계·삭제 버튼·블록 설정 패널 오버레이) |
|
||||
| components/admin/AdminPageForm.vue | 관리자 페이지 작성/수정 폼, 게시글 작성과 같은 전체 화면 에디터·상단 저장 툴바·접이식 오른쪽 설정 패널, 저장 payload 기준 미저장 비교·라우트/브라우저 이탈 확인, 페이지 공개 상태 선택, HTML 문서 기본 모드, 홈 Pages 진열장 노출 메타와 등록된 페이지 섹션 검색형 선택, 빈 본문/`!`+Tab HTML 골격 자동 완성, 항상 보이는 일반 텍스트/HTML 모드 선택, 한글 제목 영문 슬러그 자동 변환, HTML textarea 커서 위치 파일 URL 삽입, HTML 태그 내부 스마트 따옴표 정규화 |
|
||||
| components/admin/AdminMarkdownEditor.vue | 관리자 글 Markdown-first 에디터, 라이브·소스 모드 `/` 슬래시 명령(`/표` 기본 표 템플릿 포함, 후보 없는 슬래시 입력은 일반 줄바꿈·커서 이동 허용)·미디어 모달 선택 결과 본문 삽입(이미지·갤러리·비디오·오디오·파일), 코드 블록 본문 슬래시 명령 비활성화, 커서 블록 컨텍스트·선택 텍스트 색상 컨텍스트 `block-panel` emit, 소스 모드 입력 직후 textarea 최신 본문 기준 커서 논리 줄·블록 컨텍스트 갱신, 에디터 본문 `Cmd/Ctrl+Z` 히스토리와 선택 위치 복원, 라이브 이미지 설정 패널·이미지↔갤러리 드래그 변환(`merge-images-to-gallery`·`insert-image-to-gallery`·`extract-gallery-image`), 소스·라이브 `Cmd+Shift+K` 줄 삭제, 소스·라이브 `Cmd/Ctrl+K` 링크 삽입, 코드·콜아웃·토글 내부 줄 삭제, 라이브 fenced 블록 현재 닫는 줄 기준 교체, 블록 패널 바깥 클릭·지원 블록 밖 커서 이동 시 닫기·미디어 모달 중 유지, 인용 마지막 줄 아래 방향키 외부 문단 포커스 이동, 인용·콜아웃·코드·토글 선언 줄 옵션 수정, IME 조합 중 현재 블록 범위 안에서만 블록 패널 유지, 소스 모드 wrap 라인 번호 보정·라이브↔소스 위치 복원(코드·콜아웃·토글 선언 줄은 본문 줄로 보정), 라이브 슬래시 명령 후 포커스 복원 지연·미디어 명령 원문 줄 재반영 방지 |
|
||||
| components/admin/AdminMarkdownEditor.vue | 관리자 글 Markdown-first 에디터, 라이브·소스 모드 `/` 슬래시 명령(`/표` 기본 표 템플릿 포함, 후보 없는 슬래시 입력은 일반 줄바꿈·커서 이동 허용)·미디어 모달 선택 결과 본문 삽입(이미지·갤러리·비디오·오디오·파일), 코드 블록 본문 슬래시 명령 비활성화, 커서 블록 컨텍스트·선택 텍스트 색상 컨텍스트 `block-panel` emit, 소스 모드 입력 직후 textarea 최신 본문 기준 커서 논리 줄·블록 컨텍스트 갱신, 에디터 본문 `Cmd/Ctrl+Z` 히스토리와 선택 위치 복원, 라이브 이미지 설정 패널·이미지↔갤러리 드래그 변환(`merge-images-to-gallery`·`insert-image-to-gallery`·`extract-gallery-image`), 소스·라이브 `Cmd+Shift+K` 줄 삭제, 소스·라이브 `Cmd/Ctrl+K` 링크 삽입, 코드·콜아웃·토글 내부 줄 삭제, 라이브 fenced 블록 현재 닫는 줄 기준 교체, 블록 패널 바깥 클릭·지원 블록 밖 커서 이동 시 닫기·미디어 모달 중 유지, 블록 패널 옵션 적용 composable 연결, 인용 마지막 줄 아래 방향키 외부 문단 포커스 이동, 인용·콜아웃·코드·토글 선언 줄 옵션 수정, IME 조합 중 현재 블록 범위 안에서만 블록 패널 유지, 소스 모드 wrap 라인 번호 보정·라이브↔소스 위치 복원(코드·콜아웃·토글 선언 줄은 본문 줄로 보정), 라이브 슬래시 명령 후 포커스 복원 지연·미디어 명령 원문 줄 재반영 방지 |
|
||||
| components/admin/AdminEditorBlockPanel.vue | 게시물 설정 사이드바 오버레이 블록 설정(선택 텍스트 색상, 단일 이미지 대표 너비 셀렉트(전체·갤러리 2장 1칸·갤러리 3장 1칸·갤러리 3장 2칸·직접 입력)·직접 너비·이미지·갤러리·링크·iframe·HTML·인용 배경색·콜아웃 제목·아이콘 ON/OFF 토글·비활성 아이콘 영역·배경색·코드 언어 셀렉트·줄번호 ON/OFF 토글·토글), 갤러리 선택 이미지 강조 |
|
||||
| components/admin/AdminTagForm.vue | 관리자 태그 생성/수정 폼(이름/슬러그/설명/색상, 태그 헤더 이미지 업로드·삭제, 오버레이 제목·본문 편집, 수정 화면 변경 저장 버튼 변경사항 기준 활성화) |
|
||||
| components/admin/AdminMemberForm.vue | 관리자 멤버 추가/수정 폼(Ghost형 3분할, 기존 회원 보기/수정 모드 분리, 요약 1fr·입력 2fr, 원형 썸네일 hover 등록·변경·삭제, 기본 정보·레이블·관리자 노트·활동 요약, 설정 메뉴의 비밀번호 변경·멤버 삭제 모달, 저장 토스트, 미저장 변경사항 이탈 확인) |
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
- Markdown 리스트 마커는 항목이 여러 줄로 줄바꿈되어도 항목 전체 높이 중앙이 아니라 첫 줄 기준으로 정렬한다.
|
||||
- 표준 마크다운 표(`| 헤더 | ... |`, `| --- | ... |`)는 본문에서 가로 스크롤 가능한 HTML table로 렌더링한다. 정렬 구분선(`:---`, `:---:`, `---:`)은 각각 좌/중앙/우 정렬로 반영한다. 표 헤더와 셀 내부의 인라인 링크·굵게·기울임·인라인 코드를 렌더링하고, 셀 내부 `<br>` 또는 `<br/>`은 줄바꿈으로 표시한다. 링크 텍스트 안의 `**굵게**` 같은 중첩 인라인 문법도 렌더링한다.
|
||||
- 인용문(`>`)은 왼쪽 세로 막대형 기본 스타일로 표시한다. 기본 인용 텍스트는 라이트·다크 모드 모두 사이트 본문 텍스트 색상(`--site-text`)을 따른다. 첫 줄 옵션 `> [!bg=blue]` 또는 `> {bg=blue}`는 인용 막대 색상으로 반영하며, 지원 값은 콜아웃과 같은 `gray`, `blue`, `green`, `yellow`, `red`, `purple`이다. 인용 본문 안의 `::red[**텍스트**]` 같은 색상·굵게·링크 등 인라인 문법은 일반 문단과 같은 규칙으로 렌더링한다.
|
||||
- 관리자 Markdown-first 글쓰기의 오른쪽 블록 설정 패널은 인용·콜아웃·코드 블록·토글 설정과 선택 텍스트 색상 설정을 지원한다. 단일 이미지는 전체 너비(자동)·갤러리 2장 1칸(356px)·갤러리 3장 1칸(235px)·갤러리 3장 2칸(477px)·직접 입력 셀렉트로 표시 너비를 고를 수 있으며 저장 시 이미지 마크다운 뒤에 `{size=숫자}` 옵션을 붙인다. 공개 본문은 이 값을 최대 너비로 적용하고 원본 비율은 유지한다. 직접 입력은 120~1600px 범위로 저장하되 입력 중에는 범위 보정을 즉시 강제하지 않는다. 콜아웃은 제목·아이콘 표시 ON/OFF 토글·아이콘·배경색, 코드 블록은 언어 셀렉트·줄번호 표시 ON/OFF 토글, 토글은 기본 펼침·닫힘 상태를 선언 줄에 저장한다. 콜아웃 아이콘 입력·프리셋은 아이콘 표시가 꺼진 상태에서 흐린 비활성 영역으로 표시하고, 토글을 켠 뒤에만 조작한다. 코드 블록 줄번호는 기본 숨김이며, 켠 경우에만 선언 줄에 `line-numbers`를 저장한다. 일반 텍스트 범위를 선택하면 텍스트 색상 패널이 열리고 회색·파랑·초록·노랑·빨강·보라 인라인 색상을 적용한다. 콜아웃 아이콘은 라이브·공개 화면 모두 왼쪽 상단에 배치하고, 아이콘·제목 헤더 아래에 본문을 줄바꿈해 표시한다. 아이콘 미사용 시 자리 표시자를 남기지 않는다. 인용·콜아웃·토글 본문은 라이브 모드에서 굵게·링크·색상 같은 인라인 문법과 기본 불릿/번호 목록을 표시한다. 라이브 문단에서는 `>` 입력으로 인용, ``` Enter로 코드 블록, `!!!` Enter로 콜아웃을 만들 수 있고, `/표` 또는 `/table` 슬래시 명령으로 기본 3열 표 마크다운을 삽입한다. `/html`은 `:::html`, `/link`는 `:::link`, `/iframe`은 `:::iframe` fenced 블록을 삽입한다. 소스·라이브 모드 모두 텍스트를 선택한 뒤 `Cmd/Ctrl+K`를 누르면 `[선택 텍스트](https://)` 인라인 링크 마크다운을 삽입하고 URL 부분을 선택한다. 소스 textarea의 선택 치환·인라인 감싸기·링크 삽입·줄 변환 계산은 `markdown-source-selection` helper가 담당하고, 라이브 전환 포커스 대상 보정과 코드 펜스 본문 줄 판별은 `markdown-editor-focus` helper가 담당한다. 슬래시 명령의 미디어 모달·마크다운 줄 삽입 적용 계획과 적용 후 에디터 효과 계획은 `markdown-slash-commands` helper가 계산한다. 미디어 선택 모달의 상태·표시 계산, 서버 목록 조회, 업로드 실행과 업로드 후 선택 상태 갱신은 `useAdminMarkdownMediaPicker`가 관리하고, 선택 미디어 삽입 계획과 업로드 입력·응답 정규화는 `markdown-editor-media` helper가 계산한다. 오른쪽 블록 패널의 미디어·임베드·인용·콜아웃·코드·토글 옵션 줄 교체와 인라인 색상 본문 변경 계산은 `markdown-editor-block-options` helper가 담당한다. 라이브 미리보기 갤러리·이미지·문단 이벤트의 Markdown 값 변경 계산은 `markdown-editor-preview-blocks` helper가 담당한다. 에디터는 선택·업로드·옵션 변경·미리보기 이벤트 결과의 본문 반영과 실제 포커스·스크롤 실행만 처리한다. 작성/라이브 모드 전환의 pending 포커스 상태와 복원 순서는 `useAdminMarkdownFocusBridge`가 관리하고, 라이브 미리보기 표면 DOM 접근·현재 source line 계산·줄 포커스 실행은 `useAdminMarkdownPreviewFocus`가 관리한다. 에디터 되돌리기·다시 실행은 `useAdminMarkdownHistory`에서 본문 값과 선택 위치를 함께 복원해 줄 앞 이동을 줄인다. 라이브 모드에서 위·아래 방향키로 인접 블록을 이동하면 커서가 화면 밖으로 벗어날 때 편집 스크롤 영역도 함께 이동한다. 라이브 코드·인용·콜아웃·토글 블록은 맨 위/맨 아래 방향키로 외부 기본 문단을 만들며 빠져나올 수 있고, 인용 첫 글자 앞 Backspace는 일반 문단으로 되돌린다. 한글 등 IME 조합 입력 중에는 줄바꿈 직후 블록 판별이 일시적으로 비어도 마지막 블록 컨텍스트를 유지해 설정 패널이 닫히지 않게 한다. 인용 편집 중 브라우저 자동 목록 보정으로 `>. ` 접두가 섞이면 커밋 시 정리한다.
|
||||
- 관리자 Markdown-first 글쓰기의 오른쪽 블록 설정 패널은 인용·콜아웃·코드 블록·토글 설정과 선택 텍스트 색상 설정을 지원한다. 단일 이미지는 전체 너비(자동)·갤러리 2장 1칸(356px)·갤러리 3장 1칸(235px)·갤러리 3장 2칸(477px)·직접 입력 셀렉트로 표시 너비를 고를 수 있으며 저장 시 이미지 마크다운 뒤에 `{size=숫자}` 옵션을 붙인다. 공개 본문은 이 값을 최대 너비로 적용하고 원본 비율은 유지한다. 직접 입력은 120~1600px 범위로 저장하되 입력 중에는 범위 보정을 즉시 강제하지 않는다. 콜아웃은 제목·아이콘 표시 ON/OFF 토글·아이콘·배경색, 코드 블록은 언어 셀렉트·줄번호 표시 ON/OFF 토글, 토글은 기본 펼침·닫힘 상태를 선언 줄에 저장한다. 콜아웃 아이콘 입력·프리셋은 아이콘 표시가 꺼진 상태에서 흐린 비활성 영역으로 표시하고, 토글을 켠 뒤에만 조작한다. 코드 블록 줄번호는 기본 숨김이며, 켠 경우에만 선언 줄에 `line-numbers`를 저장한다. 일반 텍스트 범위를 선택하면 텍스트 색상 패널이 열리고 회색·파랑·초록·노랑·빨강·보라 인라인 색상을 적용한다. 콜아웃 아이콘은 라이브·공개 화면 모두 왼쪽 상단에 배치하고, 아이콘·제목 헤더 아래에 본문을 줄바꿈해 표시한다. 아이콘 미사용 시 자리 표시자를 남기지 않는다. 인용·콜아웃·토글 본문은 라이브 모드에서 굵게·링크·색상 같은 인라인 문법과 기본 불릿/번호 목록을 표시한다. 라이브 문단에서는 `>` 입력으로 인용, ``` Enter로 코드 블록, `!!!` Enter로 콜아웃을 만들 수 있고, `/표` 또는 `/table` 슬래시 명령으로 기본 3열 표 마크다운을 삽입한다. `/html`은 `:::html`, `/link`는 `:::link`, `/iframe`은 `:::iframe` fenced 블록을 삽입한다. 소스·라이브 모드 모두 텍스트를 선택한 뒤 `Cmd/Ctrl+K`를 누르면 `[선택 텍스트](https://)` 인라인 링크 마크다운을 삽입하고 URL 부분을 선택한다. 소스 textarea의 선택 치환·인라인 감싸기·링크 삽입·줄 변환 계산은 `markdown-source-selection` helper가 담당하고, 라이브 전환 포커스 대상 보정과 코드 펜스 본문 줄 판별은 `markdown-editor-focus` helper가 담당한다. 슬래시 명령의 미디어 모달·마크다운 줄 삽입 적용 계획과 적용 후 에디터 효과 계획은 `markdown-slash-commands` helper가 계산한다. 미디어 선택 모달의 상태·표시 계산, 서버 목록 조회, 업로드 실행과 업로드 후 선택 상태 갱신은 `useAdminMarkdownMediaPicker`가 관리하고, 선택 미디어 삽입 계획과 업로드 입력·응답 정규화는 `markdown-editor-media` helper가 계산한다. 오른쪽 블록 패널의 미디어·임베드·인용·콜아웃·코드·토글 옵션 줄 교체와 인라인 색상 본문 변경 계산은 `markdown-editor-block-options` helper가 담당하고, 실제 적용 실행은 `useAdminMarkdownBlockActions`가 관리한다. 라이브 미리보기 갤러리·이미지·문단 이벤트의 Markdown 값 변경 계산은 `markdown-editor-preview-blocks` helper가 담당한다. 에디터는 선택·업로드·옵션 변경·미리보기 이벤트 결과의 본문 반영과 실제 포커스·스크롤 실행만 처리한다. 작성/라이브 모드 전환의 pending 포커스 상태와 복원 순서는 `useAdminMarkdownFocusBridge`가 관리하고, 라이브 미리보기 표면 DOM 접근·현재 source line 계산·줄 포커스 실행은 `useAdminMarkdownPreviewFocus`가 관리한다. 에디터 되돌리기·다시 실행은 `useAdminMarkdownHistory`에서 본문 값과 선택 위치를 함께 복원해 줄 앞 이동을 줄인다. 라이브 모드에서 위·아래 방향키로 인접 블록을 이동하면 커서가 화면 밖으로 벗어날 때 편집 스크롤 영역도 함께 이동한다. 라이브 코드·인용·콜아웃·토글 블록은 맨 위/맨 아래 방향키로 외부 기본 문단을 만들며 빠져나올 수 있고, 인용 첫 글자 앞 Backspace는 일반 문단으로 되돌린다. 한글 등 IME 조합 입력 중에는 줄바꿈 직후 블록 판별이 일시적으로 비어도 마지막 블록 컨텍스트를 유지해 설정 패널이 닫히지 않게 한다. 인용 편집 중 브라우저 자동 목록 보정으로 `>. ` 접두가 섞이면 커밋 시 정리한다.
|
||||
- 게시물 상세의 오른쪽 사이드바는 데스크톱에서 블로그 소개·Follow 대신 본문 H1~H3 제목 기반 목차를 최상단에 표시한다. 목차는 별도 최대 높이와 내부 스크롤을 두지 않고 전체 목록이 먼저 펼쳐지며, 왼쪽 기준선과 브랜드 컬러 활성 항목 표시선을 둔다. 목차 항목은 최소 24px 높이와 세로 중앙 정렬을 사용하며, 2줄 이상 제목에서는 항목 높이에 맞춰 활성 표시선도 함께 늘어난다. 활성 표시선은 항목의 가상 요소 배경 막대로 그려 텍스트 시작점이 밀리지 않게 한다. 목차 링크는 본문 제목에 부여된 앵커 ID로 부드럽게 이동하며, 고정 상단 헤더 높이와 여백을 반영해 제목이 화면 밖에 걸리지 않게 한다. 본문 스크롤 중에는 현재 제목에 해당하는 목차 항목을 강조하고, 목차 항목이 많으면 오른쪽 사이드바 스크롤이 활성 항목을 따라간다. 본문 제목이 없으면 목차 없음 문구를 표시한다. 오른쪽 사이드바가 본문 아래로 내려가는 모바일 폭에서는 목차를 숨긴다. 게시물 상세에서는 일반 오른쪽 사이드 광고 대신 게시물 사이드 광고 코드를 목차 아래에 표시하고, 왼쪽 사이드바에는 게시물 광고를 표시하지 않는다. 게시물 상세 목차와 게시물 사이드 광고 슬롯은 하단 구분선을 표시하지 않는다.
|
||||
- 제목 우측 공유 버튼을 누르면 게시물 공유 모달을 연다.
|
||||
- 공개 회원 로그인 세션의 회원 ID가 게시물 `author_id`와 같으면 제목 우측 공유 버튼 옆에 수정 아이콘을 표시하며, 클릭 시 `/admin/posts/:id` 편집 화면을 새 탭으로 연다. 별도 관리자 세션만 있거나 비로그인 상태에서는 표시하지 않는다.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## v1.11 내부 개선
|
||||
|
||||
- 거대 컴포넌트 책임 분리 진행.
|
||||
- `components/admin/AdminMarkdownEditor.vue`: 오른쪽 블록 패널 이벤트 적용과 소스/라이브 공통 포커스 후처리를 단계적으로 composable 또는 하위 컴포넌트로 분리.
|
||||
- `components/admin/AdminMarkdownEditor.vue`: 소스/라이브 공통 포커스 후처리와 남은 라이브 이벤트 연결부를 단계적으로 composable 또는 하위 컴포넌트로 분리.
|
||||
- 미디어 카탈로그 전환 코드 수명 정리.
|
||||
- 운영 `MEDIA_CATALOG_MODE=database` 안정화 후 `shadow`/`filesystem` 런타임 분기 축소 가능 여부 검토.
|
||||
- fallback은 운영 안정성에 필요하므로 제거 전 reconcile·로그·테스트 기준을 먼저 정리.
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# 업데이트 이력
|
||||
|
||||
## v1.11.120
|
||||
|
||||
- 관리자 Markdown 에디터의 오른쪽 블록 패널 옵션 적용 실행을 `useAdminMarkdownBlockActions` composable로 분리.
|
||||
- 에디터 컴포넌트는 활성 블록·본문 값·포커스 실행 함수만 주입하고 이미지·갤러리·임베드·콜아웃·코드·토글·인라인 색상 적용 함수를 composable에서 받아 쓰도록 정리.
|
||||
- 블록 패널 액션 composable의 이미지 너비 저장, 파일명 캡션, 갤러리 fallback, 소스·라이브 인라인 색상 적용 단위 테스트 추가.
|
||||
|
||||
## v1.11.119
|
||||
|
||||
- 관리자 Markdown 에디터의 라이브 미리보기 갤러리·이미지·문단 이벤트 결과 계산을 `markdown-editor-preview-blocks` helper로 분리.
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.11.119",
|
||||
"version": "1.11.120",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sori.studio",
|
||||
"version": "1.11.119",
|
||||
"version": "1.11.120",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.11.119",
|
||||
"version": "1.11.120",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
|
||||
153
tests/useAdminMarkdownBlockActions.test.js
Normal file
153
tests/useAdminMarkdownBlockActions.test.js
Normal file
@@ -0,0 +1,153 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { useAdminMarkdownBlockActions } from '../composables/useAdminMarkdownBlockActions.js'
|
||||
|
||||
/**
|
||||
* 테스트용 블록 액션 상태를 만든다.
|
||||
* @param {Object} options - 테스트 옵션
|
||||
* @returns {Object} 테스트 상태
|
||||
*/
|
||||
const createBlockActionsState = (options = {}) => {
|
||||
const calls = {
|
||||
applyPlans: [],
|
||||
ensure: 0,
|
||||
focusPreview: [],
|
||||
insertGallery: [],
|
||||
previewFocusRequests: [],
|
||||
textareaSelections: []
|
||||
}
|
||||
const value = ref(options.value ?? '')
|
||||
const activeMediaBlock = ref(options.activeMediaBlock ?? {
|
||||
kind: 'image',
|
||||
startLine: 0,
|
||||
endLine: 0,
|
||||
images: [{ alt: '기존', url: '/old.png' }]
|
||||
})
|
||||
const activeBlockContext = ref(options.activeBlockContext ?? activeMediaBlock.value)
|
||||
const inlineColorSelectionContext = ref(options.inlineColorSelectionContext ?? null)
|
||||
const actions = useAdminMarkdownBlockActions({
|
||||
value,
|
||||
activeMediaBlock,
|
||||
activeBlockContext,
|
||||
inlineColorSelectionContext,
|
||||
applyLineReplacementPlan: (plan, focusEditor = false) => {
|
||||
calls.applyPlans.push({ plan, focusEditor })
|
||||
|
||||
if (!plan) {
|
||||
return false
|
||||
}
|
||||
|
||||
const lines = String(value.value || '').split('\n')
|
||||
lines.splice(plan.startLine, plan.endLine - plan.startLine + 1, ...plan.replacementLines)
|
||||
value.value = lines.join('\n')
|
||||
return true
|
||||
},
|
||||
ensureBlockPanelEngaged: () => {
|
||||
calls.ensure += 1
|
||||
},
|
||||
insertGallery: (images) => {
|
||||
calls.insertGallery.push(images)
|
||||
},
|
||||
setTextareaSelection: (...args) => {
|
||||
calls.textareaSelections.push(args)
|
||||
},
|
||||
requestPreviewFocus: (focus) => {
|
||||
calls.previewFocusRequests.push(focus)
|
||||
},
|
||||
focusPreviewEditableAtLine: (...args) => {
|
||||
calls.focusPreview.push(args)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
actions,
|
||||
activeBlockContext,
|
||||
activeMediaBlock,
|
||||
calls,
|
||||
inlineColorSelectionContext,
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
test('이미지 표시 너비 변경을 현재 미디어 줄에 적용한다', () => {
|
||||
const state = createBlockActionsState()
|
||||
|
||||
state.actions.updateActiveMediaImage(0, { pixelWidth: 240 })
|
||||
|
||||
assert.equal(state.value.value, '{size=240}')
|
||||
assert.equal(state.calls.ensure, 1)
|
||||
assert.equal(state.calls.applyPlans.at(-1).focusEditor, false)
|
||||
})
|
||||
|
||||
test('파일명 캡션 사용은 이미지 URL 기준 기본 캡션을 채운다', () => {
|
||||
const state = createBlockActionsState({
|
||||
value: '',
|
||||
activeMediaBlock: {
|
||||
kind: 'image',
|
||||
startLine: 0,
|
||||
endLine: 0,
|
||||
images: [{ alt: '', caption: '', url: '/uploads/posts/2026/07/sample.png' }]
|
||||
}
|
||||
})
|
||||
|
||||
state.actions.setActiveMediaUseAlt(0, true)
|
||||
|
||||
assert.equal(state.value.value, '')
|
||||
assert.equal(state.calls.ensure, 1)
|
||||
})
|
||||
|
||||
test('활성 갤러리가 없으면 갤러리 삽입 함수로 위임한다', () => {
|
||||
const state = createBlockActionsState({
|
||||
activeMediaBlock: null
|
||||
})
|
||||
const images = [{ url: '/a.png', alt: 'A' }]
|
||||
|
||||
state.actions.appendImagesToActiveGallery(images)
|
||||
|
||||
assert.deepEqual(state.calls.insertGallery, [images])
|
||||
assert.equal(state.calls.applyPlans.length, 0)
|
||||
})
|
||||
|
||||
test('소스 모드 인라인 색상 적용 후 textarea 선택 범위를 갱신한다', () => {
|
||||
const state = createBlockActionsState({
|
||||
value: '강조 문장',
|
||||
inlineColorSelectionContext: {
|
||||
kind: 'inline-color',
|
||||
mode: 'source',
|
||||
start: 0,
|
||||
end: 2,
|
||||
text: '강조'
|
||||
}
|
||||
})
|
||||
|
||||
state.actions.updateActiveInlineColor('red')
|
||||
|
||||
assert.equal(state.value.value, '::red[강조] 문장')
|
||||
assert.equal(state.inlineColorSelectionContext.value, null)
|
||||
assert.deepEqual(state.calls.textareaSelections, [[9, 9, { preserveScroll: true }]])
|
||||
assert.equal(state.calls.ensure, 1)
|
||||
})
|
||||
|
||||
test('라이브 모드 인라인 색상 적용 후 미리보기 포커스를 예약한다', async () => {
|
||||
const state = createBlockActionsState({
|
||||
value: '> 문장 테스트',
|
||||
inlineColorSelectionContext: {
|
||||
kind: 'inline-color',
|
||||
mode: 'live',
|
||||
sourceLine: 0,
|
||||
startOffset: 0,
|
||||
endOffset: 2,
|
||||
bodyText: '문장 테스트',
|
||||
text: '문장'
|
||||
}
|
||||
})
|
||||
|
||||
state.actions.updateActiveInlineColor('blue')
|
||||
await nextTick()
|
||||
|
||||
assert.equal(state.value.value, '> ::blue[문장] 테스트')
|
||||
assert.equal(state.inlineColorSelectionContext.value, null)
|
||||
assert.deepEqual(state.calls.previewFocusRequests, [{ line: 0, offset: 2 }])
|
||||
assert.deepEqual(state.calls.focusPreview, [[0, 0, 'auto', 2, 'nearest']])
|
||||
})
|
||||
Reference in New Issue
Block a user