From 675c45092bfc779fb25c2ff3aef650b4c2113ced Mon Sep 17 00:00:00 2001 From: zenn Date: Mon, 27 Jul 2026 11:22:51 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8A=AC=EB=9E=98=EC=8B=9C=20=EB=AA=85?= =?UTF-8?q?=EB=A0=B9=20=EC=BB=A4=EC=84=9C=20=EB=B3=B5=EC=9B=90=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/admin/AdminMarkdownEditor.vue | 11 ++++--- docs/map.md | 4 +-- docs/update.md | 5 ++++ lib/markdown-slash-commands.js | 22 ++++++++++++++ package-lock.json | 4 +-- package.json | 2 +- tests/markdown-slash-commands.test.js | 38 ++++++++++++++++++++++++ 7 files changed, 77 insertions(+), 9 deletions(-) diff --git a/components/admin/AdminMarkdownEditor.vue b/components/admin/AdminMarkdownEditor.vue index 5b194dc..d17b588 100644 --- a/components/admin/AdminMarkdownEditor.vue +++ b/components/admin/AdminMarkdownEditor.vue @@ -46,6 +46,7 @@ import { import { filterSlashCommands, getSlashCommandFocusTarget, + getSlashCommandTextareaSelection, insertMarkdownLinesAtSlashTarget, isMediaSlashAction, parseSlashInput, @@ -2648,10 +2649,12 @@ const focusPreviewAfterSlashCommand = (line, replacementLines) => { * @returns {void} */ const focusTextareaAfterSlashCommand = (line, replacementLines) => { - const value = markdownValue.value ?? '' - const lineStart = value.split('\n').slice(0, line).join('\n').length + (line > 0 ? 1 : 0) - const { offset } = getSlashCommandFocusTarget(line, replacementLines) - setTextareaSelection(lineStart + offset, lineStart + offset) + const selection = getSlashCommandTextareaSelection({ + markdown: markdownValue.value ?? '', + startLine: line, + lines: replacementLines + }) + setTextareaSelection(selection.start, selection.end) } /** diff --git a/docs/map.md b/docs/map.md index 39cba3d..3daddeb 100644 --- a/docs/map.md +++ b/docs/map.md @@ -60,7 +60,7 @@ | lib/markdown-image.js | 이미지 마크다운 직렬화·파싱, 단독 이미지 URL 판별 | | lib/markdown-table.js | 공개 Markdown 표 행·구분선·정렬·블록 파싱 | | lib/markdown-toc.js | 공개 게시글 TOC용 H1~H3 제목 추출과 앵커 ID 생성 | -| lib/markdown-slash-commands.js | 관리자 Markdown-first 에디터 슬래시 명령 목록과 삽입 줄 정의, 미디어 명령 action 판별, 미디어 명령 지연 commit 무시·슬래시 원문 줄 교체 삽입 helper | +| lib/markdown-slash-commands.js | 관리자 Markdown-first 에디터 슬래시 명령 목록과 삽입 줄 정의, 미디어 명령 action 판별, 미디어 명령 지연 commit 무시·슬래시 원문 줄 교체 삽입, 적용 후 포커스·textarea 선택 위치 계산 helper | | lib/markdown-slash-menu-position.js | 관리자 Markdown-first 에디터 슬래시 메뉴 뷰포트 위치 보정 helper | | lib/markdown-slash-menu-scroll.js | 관리자 Markdown-first 에디터 슬래시 메뉴 선택 항목 표시용 scrollTop 계산 helper | | lib/markdown-inline.js | 공개·관리자 공통 인라인 마크다운 파서, 링크·굵게·기울임·인라인 코드·첨자·인라인 색상 토큰(`::color[텍스트]`) 변환, 링크·굵게·기울임·색상 내부 중첩 인라인 문법과 `
` 줄바꿈 처리, 라이브 멀티라인 본문의 기본 불릿/번호 목록 HTML 변환과 contenteditable 목록 마커 기준 커서 offset 계산·직렬화 | @@ -123,7 +123,7 @@ | tests/markdown-block-context.test.js | 관리자 블록 설정 패널의 활성 블록 컨텍스트 단위 회귀 테스트 | | tests/markdown-editor-history.test.js | 관리자 Markdown 에디터 undo/redo 히스토리 항목과 선택 범위 보정 단위 회귀 테스트 | | tests/markdown-editor-media.test.js | 관리자 Markdown 에디터 미디어 선택 helper의 종류·선택 가능 여부·미리보기·선택 상태·블록 생성 단위 회귀 테스트 | -| tests/markdown-slash-commands.test.js | 관리자 슬래시 명령 파싱·미디어 action·포커스 대상·원문 줄 교체 삽입 단위 회귀 테스트 | +| tests/markdown-slash-commands.test.js | 관리자 슬래시 명령 파싱·미디어 action·포커스 대상·textarea 선택 위치·원문 줄 교체 삽입 단위 회귀 테스트 | | tests/markdown-slash-menu-scroll.test.js | 관리자 슬래시 메뉴 선택 항목 scrollTop 보정 단위 테스트 | | tests/markdown-block-parser.test.js | 공개 Markdown 본문 블록 파싱 단위 회귀 테스트 | | tests/markdown-fenced-meta.test.js | Markdown fenced 블록 시작·종료 줄 탐색, 본문 수집, 메타 파싱 단위 회귀 테스트 | diff --git a/docs/update.md b/docs/update.md index 6b990ba..9cb6a97 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,10 @@ # 업데이트 이력 +## v1.11.87 + +- 관리자 Markdown 에디터의 슬래시 명령 textarea 커서 복원 계산을 `markdown-slash-commands` helper로 분리. +- 슬래시 명령 단일 줄·여러 줄 삽입 후 textarea 선택 위치 단위 테스트 추가. + ## v1.11.86 - 관리자 Markdown 에디터의 슬래시 메뉴 상태 초기화 로직을 `clearLiveSlashState`로 통합. diff --git a/lib/markdown-slash-commands.js b/lib/markdown-slash-commands.js index e4ea9b9..0734843 100644 --- a/lib/markdown-slash-commands.js +++ b/lib/markdown-slash-commands.js @@ -279,3 +279,25 @@ export const getSlashCommandFocusTarget = (startLine, lines) => { return { line: safeStart + 1, offset: 0 } } + +/** + * 슬래시 명령 적용 후 textarea 선택 위치를 계산한다. + * @param {Object} options - 계산 옵션 + * @param {string} options.markdown - 현재 마크다운 + * @param {number} options.startLine - 교체 시작 줄(0-based) + * @param {string[]} options.lines - 삽입 줄 + * @returns {{ start: number, end: number }} 선택 범위 + */ +export const getSlashCommandTextareaSelection = ({ markdown, startLine, lines }) => { + const sourceLines = String(markdown ?? '').split('\n') + const safeStart = Math.max(0, Math.min(Number.isFinite(startLine) ? startLine : 0, sourceLines.length)) + const { line, offset } = getSlashCommandFocusTarget(safeStart, lines) + const safeLine = Math.max(0, Math.min(line, sourceLines.length)) + const lineStart = sourceLines.slice(0, safeLine).join('\n').length + (safeLine > 0 ? 1 : 0) + const selection = lineStart + offset + + return { + start: selection, + end: selection + } +} diff --git a/package-lock.json b/package-lock.json index 65bac5c..2b89016 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sori.studio", - "version": "1.11.86", + "version": "1.11.87", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sori.studio", - "version": "1.11.86", + "version": "1.11.87", "hasInstallScript": true, "dependencies": { "@nuxtjs/tailwindcss": "^6.14.0", diff --git a/package.json b/package.json index 3d2a3d1..e92c80e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sori.studio", - "version": "1.11.86", + "version": "1.11.87", "private": true, "type": "module", "imports": { diff --git a/tests/markdown-slash-commands.test.js b/tests/markdown-slash-commands.test.js index d6a491b..8bac04b 100644 --- a/tests/markdown-slash-commands.test.js +++ b/tests/markdown-slash-commands.test.js @@ -2,6 +2,7 @@ import assert from 'node:assert/strict' import test from 'node:test' import { getSlashCommandFocusTarget, + getSlashCommandTextareaSelection, insertMarkdownLinesAtSlashTarget, isMediaSlashAction, parseSlashInput, @@ -71,3 +72,40 @@ test('슬래시 명령 포커스 대상은 단일 줄과 블록 삽입을 구분 offset: 0 }) }) + +test('슬래시 명령 textarea 선택 위치는 줄 시작과 포커스 오프셋을 합산한다', () => { + const markdown = [ + '첫 줄', + '둘째 줄', + '## ', + '마지막 줄' + ].join('\n') + + assert.deepEqual(getSlashCommandTextareaSelection({ + markdown, + startLine: 2, + lines: ['## '] + }), { + start: 12, + end: 12 + }) +}) + +test('슬래시 명령 textarea 선택 위치는 여러 줄 블록의 내부 빈 줄을 가리킨다', () => { + const markdown = [ + '첫 줄', + '```', + '', + '```', + '마지막 줄' + ].join('\n') + + assert.deepEqual(getSlashCommandTextareaSelection({ + markdown, + startLine: 1, + lines: ['```', '', '```'] + }), { + start: 8, + end: 8 + }) +})