슬래시 명령 로직 분리

This commit is contained in:
2026-07-24 17:27:24 +09:00
parent 7bc04ec5b9
commit 31cae6f90b
7 changed files with 135 additions and 17 deletions

View File

@@ -19,8 +19,11 @@ import {
import {
filterSlashCommands,
getSlashCommandFocusTarget,
insertMarkdownLinesAtSlashTarget,
isMediaSlashAction,
parseSlashInput,
resolveSlashCommand
resolveSlashCommand,
shouldIgnorePendingMediaSlashCommit
} from '../../lib/markdown-slash-commands.js'
import { buildCalloutOpenerLine, CALLOUT_BACKGROUND_OPTIONS, QUOTE_BACKGROUND_OPTIONS } from '../../lib/markdown-callout.js'
import { buildCodeFenceOpener } from '../../lib/markdown-code-block.js'
@@ -2340,9 +2343,11 @@ const onPreviewBlockContentChange = ({ startLine, endLine, replacementLines }) =
return
}
if (liveSlashInsertLine.value === startLine
&& replacementLines.length === 1
&& parseSlashInput(replacementLines[0])) {
if (shouldIgnorePendingMediaSlashCommit({
pendingInsertLine: liveSlashInsertLine.value,
startLine,
replacementLines
})) {
return
}
@@ -2794,7 +2799,7 @@ const applyLiveSlashCommand = (command) => {
const line = liveSlashSourceLine.value
if (['media-image', 'media-gallery', 'media-video', 'media-audio', 'media-file'].includes(command.action)) {
if (isMediaSlashAction(command.action)) {
liveSlashInsertLine.value = line
replaceLineRange(line, line, [], false)
onLiveSlashEnd()
@@ -2843,14 +2848,7 @@ const onLiveSlashApply = (payload) => {
* @returns {void}
*/
const insertMarkdownAtLine = (lineIndex, insertLines) => {
const sourceLines = (markdownValue.value ?? '').split('\n')
const replaceCount = parseSlashInput(sourceLines[lineIndex] ?? '') ? 1 : 0
markdownValue.value = [
...sourceLines.slice(0, lineIndex),
...insertLines,
...sourceLines.slice(lineIndex + replaceCount)
].join('\n')
markdownValue.value = insertMarkdownLinesAtSlashTarget(markdownValue.value, lineIndex, insertLines)
}
const onPreviewMergeWithPreviousLine = ({ lineIndex, mergedLine }) => {

View File

@@ -58,7 +58,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 에디터 슬래시 명령 목록과 삽입 줄 정의 |
| lib/markdown-slash-commands.js | 관리자 Markdown-first 에디터 슬래시 명령 목록과 삽입 줄 정의, 미디어 명령 action 판별, 미디어 명령 지연 commit 무시·슬래시 원문 줄 교체 삽입 helper |
| lib/markdown-inline.js | 공개·관리자 공통 인라인 마크다운 파서, 링크·굵게·기울임·인라인 코드·첨자·인라인 색상 토큰(`::color[텍스트]`) 변환, 링크·굵게·기울임·색상 내부 중첩 인라인 문법과 `<br>` 줄바꿈 처리, 라이브 멀티라인 본문의 기본 불릿/번호 목록 HTML 변환과 contenteditable 목록 마커 기준 커서 offset 계산·직렬화 |
| lib/post-tag-limit.js | 게시물별 태그 최대 개수 기본값·허용 범위·정규화 |
| lib/analytics-shared.js | 통계 추적 경로 필터·체류/스크롤 상수(클라이언트·서버 공용) |
@@ -117,6 +117,7 @@
| tests/content-gallery-images.test.js | 공개 본문 라이트박스 이미지 목록과 갤러리 행 분할 단위 회귀 테스트 |
| tests/content-image-urls.test.js | 공개 본문 이미지 파생 URL·srcset·갤러리 sizes 단위 회귀 테스트 |
| tests/markdown-block-context.test.js | 관리자 블록 설정 패널의 활성 블록 컨텍스트 단위 회귀 테스트 |
| tests/markdown-slash-commands.test.js | 관리자 슬래시 명령 파싱·미디어 action·포커스 대상·원문 줄 교체 삽입 단위 회귀 테스트 |
| tests/markdown-block-parser.test.js | 공개 Markdown 본문 블록 파싱 단위 회귀 테스트 |
| tests/markdown-fenced-meta.test.js | Markdown fenced 블록 시작·종료 줄 탐색, 본문 수집, 메타 파싱 단위 회귀 테스트 |
| tests/markdown-quote.test.js | Markdown 인용 옵션 파싱과 편집 줄 변환 단위 회귀 테스트 |

View File

@@ -1,5 +1,10 @@
# 업데이트 이력
## v1.11.67
- 관리자 Markdown 에디터의 슬래시 명령 미디어 액션 판별·지연 commit 무시·명령 줄 교체 삽입 로직을 공통 helper로 분리.
- 슬래시 명령 파싱·미디어 액션·포커스 대상·미디어 삽입 줄 교체 단위 테스트 추가.
## v1.11.66
- 라이브 작성 모드에서 오른쪽 코드 블록 설정의 줄번호 토글을 누르면 패널이 닫히는 문제 수정.

View File

@@ -219,6 +219,47 @@ export const resolveSlashCommand = (query) => {
return exact ?? matches[0]
}
/**
* 미디어 선택 모달을 여는 슬래시 명령인지 확인한다.
* @param {string} action - 명령 action
* @returns {boolean} 미디어 명령 여부
*/
export const isMediaSlashAction = (action) => ['media-image', 'media-gallery', 'media-video', 'media-audio', 'media-file']
.includes(String(action || ''))
/**
* 라이브 모드 미디어 슬래시 명령 처리 중 지연 commit을 무시할지 확인한다.
* @param {Object} options - 판별 옵션
* @param {number|null} options.pendingInsertLine - 미디어 삽입 대기 줄
* @param {number} options.startLine - commit 시작 줄
* @param {string[]} options.replacementLines - commit 대체 줄
* @returns {boolean} 무시 여부
*/
export const shouldIgnorePendingMediaSlashCommit = ({ pendingInsertLine, startLine, replacementLines }) => pendingInsertLine === startLine
&& Array.isArray(replacementLines)
&& replacementLines.length === 1
&& Boolean(parseSlashInput(replacementLines[0]))
/**
* 지정 줄에 마크다운 줄을 삽입하되, 해당 줄이 슬래시 명령이면 교체한다.
* @param {string} markdown - 원본 마크다운
* @param {number} lineIndex - 삽입 위치
* @param {string[]} insertLines - 삽입 줄
* @returns {string} 갱신된 마크다운
*/
export const insertMarkdownLinesAtSlashTarget = (markdown, lineIndex, insertLines) => {
const sourceLines = String(markdown ?? '').split('\n')
const safeLine = Math.max(0, Math.min(Number.isFinite(lineIndex) ? lineIndex : 0, sourceLines.length))
const lines = Array.isArray(insertLines) ? insertLines : []
const replaceCount = parseSlashInput(sourceLines[safeLine] ?? '') ? 1 : 0
return [
...sourceLines.slice(0, safeLine),
...lines,
...sourceLines.slice(safeLine + replaceCount)
].join('\n')
}
/**
* 슬래시 명령 적용 후 포커스할 줄·커서 오프셋을 계산한다.
* @param {number} startLine - 교체 시작 줄(0-based)

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "sori.studio",
"version": "1.11.66",
"version": "1.11.67",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sori.studio",
"version": "1.11.66",
"version": "1.11.67",
"hasInstallScript": true,
"dependencies": {
"@nuxtjs/tailwindcss": "^6.14.0",

View File

@@ -1,6 +1,6 @@
{
"name": "sori.studio",
"version": "1.11.66",
"version": "1.11.67",
"private": true,
"type": "module",
"imports": {

View File

@@ -0,0 +1,73 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
getSlashCommandFocusTarget,
insertMarkdownLinesAtSlashTarget,
isMediaSlashAction,
parseSlashInput,
resolveSlashCommand,
shouldIgnorePendingMediaSlashCommit
} from '../lib/markdown-slash-commands.js'
test('슬래시 입력은 단일 줄 명령만 파싱한다', () => {
assert.deepEqual(parseSlashInput('/image'), {
query: 'image',
raw: '/image'
})
assert.equal(parseSlashInput(' /image'), null)
assert.equal(parseSlashInput('/image\n다음 줄'), null)
})
test('정확한 슬래시 명령과 미디어 액션을 판별한다', () => {
assert.equal(resolveSlashCommand('image')?.action, 'media-image')
assert.equal(resolveSlashCommand('img')?.id, 'image')
assert.equal(isMediaSlashAction('media-image'), true)
assert.equal(isMediaSlashAction('lines'), false)
})
test('미디어 슬래시 명령 지연 commit은 같은 줄 slash 원문일 때만 무시한다', () => {
assert.equal(shouldIgnorePendingMediaSlashCommit({
pendingInsertLine: 3,
startLine: 3,
replacementLines: ['/image']
}), true)
assert.equal(shouldIgnorePendingMediaSlashCommit({
pendingInsertLine: 3,
startLine: 4,
replacementLines: ['/image']
}), false)
assert.equal(shouldIgnorePendingMediaSlashCommit({
pendingInsertLine: 3,
startLine: 3,
replacementLines: ['본문']
}), false)
})
test('슬래시 명령 줄에 미디어를 삽입할 때 원문 줄을 교체한다', () => {
const markdown = [
'첫 줄',
'/image',
'마지막 줄'
].join('\n')
assert.equal(insertMarkdownLinesAtSlashTarget(markdown, 1, ['![](/uploads/a.png)']), [
'첫 줄',
'![](/uploads/a.png)',
'마지막 줄'
].join('\n'))
})
test('일반 줄에는 지정 위치 앞에 마크다운 줄을 삽입한다', () => {
assert.equal(insertMarkdownLinesAtSlashTarget('첫 줄\n마지막 줄', 1, ['중간 줄']), '첫 줄\n중간 줄\n마지막 줄')
})
test('슬래시 명령 포커스 대상은 단일 줄과 블록 삽입을 구분한다', () => {
assert.deepEqual(getSlashCommandFocusTarget(2, ['## ']), {
line: 2,
offset: 3
})
assert.deepEqual(getSlashCommandFocusTarget(2, ['```', '', '```']), {
line: 3,
offset: 0
})
})