라이브 편집 인용과 멀티라인 입력 보정

This commit is contained in:
2026-06-05 10:50:56 +09:00
parent 09b6c51048
commit 56a2c23471
9 changed files with 121 additions and 16 deletions

View File

@@ -84,6 +84,15 @@ const normalizeBodyLines = (payload) => {
const onBodyCommit = (payload) => { const onBodyCommit = (payload) => {
commitCalloutLines(normalizeBodyLines(payload)) commitCalloutLines(normalizeBodyLines(payload))
} }
/**
* 본문 입력 중 마크다운을 동기화한다.
* @param {string|{ value?: string }} payload - 편집 페이로드
* @returns {void}
*/
const onBodyInput = (payload) => {
commitCalloutLines(normalizeBodyLines(payload))
}
</script> </script>
<template> <template>
@@ -105,6 +114,7 @@ const onBodyCommit = (payload) => {
:source-line="bodySourceLine" :source-line="bodySourceLine"
:source-line-count="bodyLines.length" :source-line-count="bodyLines.length"
:model-value="modelValue" :model-value="modelValue"
@input="onBodyInput"
@commit="onBodyCommit" @commit="onBodyCommit"
@delete-line="emit('delete-line', $event)" @delete-line="emit('delete-line', $event)"
@insert-below="emit('insert-below', $event)" @insert-below="emit('insert-below', $event)"

View File

@@ -88,6 +88,7 @@ const onBodyCommit = (body) => {
*/ */
const onBodyInput = (body) => { const onBodyInput = (body) => {
liveBody.value = body liveBody.value = body
commitCodeBlock(body)
} }
/** /**

View File

@@ -237,6 +237,7 @@ const syncSlashState = () => {
*/ */
const onEditorInput = () => { const onEditorInput = () => {
syncSlashState() syncSlashState()
emit('input', readEditorValue())
} }
/** /**
@@ -859,6 +860,12 @@ const onKeydown = (event) => {
&& props.sourceLine !== null && props.sourceLine !== null
&& readEditorValue().trim() && readEditorValue().trim()
) { ) {
const lineContext = getCaretLineContext()
if (resolvedEnterMode.value === 'multiline' && !lineContext.isFirstLine) {
return
}
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
emit('merge-with-previous', buildInsertBelowPayload()) emit('merge-with-previous', buildInsertBelowPayload())
@@ -874,6 +881,11 @@ const onKeydown = (event) => {
&& !readEditorValue().trim() && !readEditorValue().trim()
) { ) {
const lineContext = getCaretLineContext() const lineContext = getCaretLineContext()
if (resolvedEnterMode.value === 'multiline' && !lineContext.isFirstLine) {
return
}
const sourceLine = resolvedEnterMode.value === 'multiline' const sourceLine = resolvedEnterMode.value === 'multiline'
? props.sourceLine + lineContext.lineIndex ? props.sourceLine + lineContext.lineIndex
: props.sourceLine : props.sourceLine

View File

@@ -1420,6 +1420,22 @@ const onSpacerInlineCommit = (block, text) => {
commitInlineBlockLines(block, [text]) commitInlineBlockLines(block, [text])
} }
/**
* 문단 입력 중 블록 단축 변환을 처리한다.
* @param {Object} block - 문단 블록
* @param {string} text - 입력 텍스트
* @returns {void}
*/
const onParagraphLiveInput = (block, text) => {
if (String(text ?? '').trim() !== '>') {
return
}
pendingFocusLine.value = block.meta.startLine
pendingFocusPosition.value = 'start'
commitInlineBlockLines(block, ['> '])
}
/** /**
* 문단 Enter 분리 결과 줄 배열을 만든다. * 문단 Enter 분리 결과 줄 배열을 만든다.
* @param {string} head - 커서 앞 텍스트 * @param {string} head - 커서 앞 텍스트
@@ -1878,6 +1894,65 @@ const onQuoteLineInsertBelow = (block, lineIndex, payload) => {
commitInlineBlockLines(block, nextLines) commitInlineBlockLines(block, nextLines)
} }
/**
* 인용 블록 원본에서 옵션 선언 줄을 반환한다.
* @param {Object} block - 인용 블록
* @returns {string[]} 옵션 선언 줄
*/
const getQuoteOptionLines = (block) => {
const contentStartLine = getQuoteContentStartLine(block)
if (contentStartLine <= block.meta.startLine) {
return []
}
return getBlockSourceLines(block).slice(0, contentStartLine - block.meta.startLine)
}
/**
* 인용 본문 문자열을 마크다운 줄로 변환한다.
* @param {Object} block - 인용 블록
* @param {string|{ value?: string }} payload - 편집 페이로드
* @returns {string[]} 인용 마크다운 줄
*/
const buildQuoteBlockLines = (block, payload) => {
const value = typeof payload === 'string'
? payload
: String(payload?.value ?? '')
const bodyLines = String(value ?? '').replace(/\r/g, '').split('\n')
const normalizedBodyLines = bodyLines.length ? bodyLines : ['']
return [
...getQuoteOptionLines(block),
...normalizedBodyLines.map((line) => formatQuoteLine(line, false))
]
}
/**
* 인용 블록 편집 반영
* @param {Object} block - 인용 블록
* @param {string|{ value?: string }} payload - 편집 페이로드
* @returns {void}
*/
const onQuoteBlockCommit = (block, payload) => {
commitInlineBlockLines(block, buildQuoteBlockLines(block, payload))
}
/**
* 인용 블록 마지막 줄에서 아래로 이탈한다.
* @param {Object} block - 인용 블록
* @param {string|Object} payload - insert-below 페이로드
* @returns {void}
*/
const onQuoteBlockInsertBelow = (block, payload) => {
const { value } = normalizeInsertBelowPayload(payload)
onQuoteBlockCommit(block, value)
pendingFocusPosition.value = 'start'
pendingFocusOffset.value = 0
onInsertBelowBlock(block, { lines: [''] })
}
/** /**
* 목록 항목 인라인 편집 반영 * 목록 항목 인라인 편집 반영
* @param {Object} block - 블록 * @param {Object} block - 블록
@@ -2533,6 +2608,7 @@ onBeforeUnmount(() => {
:slash-command-suppressed="slashSuppressedLines.includes(block.meta.startLine)" :slash-command-suppressed="slashSuppressedLines.includes(block.meta.startLine)"
:source-line="block.meta.startLine" :source-line="block.meta.startLine"
:model-value="''" :model-value="''"
@input="onParagraphLiveInput(block, $event)"
@commit="onSpacerInlineCommit(block, $event)" @commit="onSpacerInlineCommit(block, $event)"
@split="onParagraphSplit(block, $event)" @split="onParagraphSplit(block, $event)"
@delete-line="onDeleteLine" @delete-line="onDeleteLine"
@@ -2570,20 +2646,17 @@ onBeforeUnmount(() => {
:data-source-line="block.meta.startLine" :data-source-line="block.meta.startLine"
> >
<ContentMarkdownEditableInline <ContentMarkdownEditableInline
v-for="quoteLine in getQuoteLineEntries(block)"
:key="`quote-line-${quoteLine.sourceLine}`"
block-class="content-markdown-renderer__quote-line" block-class="content-markdown-renderer__quote-line"
:model-value="quoteLine.text" :model-value="block.text"
enter-mode="insert-below" enter-mode="multiline"
allow-raw-toggle plain-text
arrow-exit-creates-line arrow-exit-creates-line
:raw-line="getMarkdownLine(quoteLine.sourceLine)" :source-line="getQuoteContentStartLine(block)"
:source-line="quoteLine.sourceLine" :source-line-count="getQuoteLineEntries(block).length"
@commit="onQuoteLineInlineCommit(block, quoteLine.sourceIndex, $event)" @input="onQuoteBlockCommit(block, $event)"
@insert-below="onQuoteLineInsertBelow(block, quoteLine.sourceIndex, $event)" @commit="onQuoteBlockCommit(block, $event)"
@insert-below="onQuoteBlockInsertBelow(block, $event)"
@delete-line="onDeleteLine" @delete-line="onDeleteLine"
@merge-with-previous="onMergeWithPreviousLine(quoteLine.sourceLine, $event)"
@raw-mode="onInlineRawMode"
/> />
</ProseBlockquote> </ProseBlockquote>
<ProseBlockquote <ProseBlockquote
@@ -2966,6 +3039,7 @@ onBeforeUnmount(() => {
:slash-command-suppressed="slashSuppressedLines.includes(block.meta.startLine)" :slash-command-suppressed="slashSuppressedLines.includes(block.meta.startLine)"
:source-line="block.meta.startLine" :source-line="block.meta.startLine"
:model-value="block.text" :model-value="block.text"
@input="onParagraphLiveInput(block, $event)"
@commit="onParagraphInlineCommit(block, $event)" @commit="onParagraphInlineCommit(block, $event)"
@split="onParagraphSplit(block, $event)" @split="onParagraphSplit(block, $event)"
@delete-line="onDeleteLine" @delete-line="onDeleteLine"

View File

@@ -112,7 +112,7 @@
| 파일 | 화면 위치 | | 파일 | 화면 위치 |
|------|-----------| |------|-----------|
| components/content/ContentRenderer.vue | 게시물/페이지 본문 | | components/content/ContentRenderer.vue | 게시물/페이지 본문 |
| components/content/ContentMarkdownRenderer.vue | 마크다운 문자열 기반 본문 렌더링, 문단 text-base(16px), 빈 줄 spacer 보존·hard break `<br>` 처리, 확장 블록 파싱, `:::` fenced 블록 원본 범위 보정, 닫히지 않은 코드 펜스 하위 콘텐츠 보호, 인용 막대 색상 옵션(`> [!bg=...]`), 라이브 문단 ` ``` `·`!!!` Enter 코드 블록·콜아웃 단축 생성, 라이브 콜아웃·인용 포커스 기반 오른쪽 설정 패널 연결, 라이브 인용 Enter 줄 추가·마지막 줄 아래 방향키 이탈, 콜아웃 아래 방향키 새 줄 생성 차단, 라이브 멀티라인 콜아웃 편집 줄 범위 포커스, 라이브 방향키 이동 시 편집 가능한 줄·카드형 블록 탐색, 라이브 코드·콜아웃·토글 내부 줄 삭제와 마지막 줄 블록 삭제, 라이브 이미지·갤러리 드래그 병합·추가·분리 UI, 갤러리 비율 기반 행 레이아웃, 라이브 갤러리 개별 이미지 편집·삭제, 리스트 마커 파란 계열 통일 | | components/content/ContentMarkdownRenderer.vue | 마크다운 문자열 기반 본문 렌더링, 문단 text-base(16px), 빈 줄 spacer 보존·hard break `<br>` 처리, 확장 블록 파싱, `:::` fenced 블록 원본 범위 보정, 닫히지 않은 코드 펜스 하위 콘텐츠 보호, 인용 막대 색상 옵션(`> [!bg=...]`), 라이브 문단 `>` 즉시 인용 변환과 ` ``` `·`!!!` Enter 코드 블록·콜아웃 단축 생성, 라이브 콜아웃·인용 포커스 기반 오른쪽 설정 패널 연결, 라이브 인용·콜아웃 멀티라인 편집 줄 범위 포커스와 마지막 줄 아래 방향키 이탈, 라이브 방향키 이동 시 편집 가능한 줄·카드형 블록 탐색, 라이브 코드·콜아웃·토글 내부 줄 삭제와 마지막 줄 블록 삭제, 라이브 이미지·갤러리 드래그 병합·추가·분리 UI, 갤러리 비율 기반 행 레이아웃, 라이브 갤러리 개별 이미지 편집·삭제, 리스트 마커 파란 계열 통일 |
| components/content/ProseHeading.vue | h1~h6 제목, 기본 mt-12 제거 | | components/content/ProseHeading.vue | h1~h6 제목, 기본 mt-12 제거 |
| components/content/ProseImage.vue | 본문 내 이미지, 로드 실패·빈 URL placeholder | | components/content/ProseImage.vue | 본문 내 이미지, 로드 실패·빈 URL placeholder |
| components/content/ProseList.vue | 목록 | | components/content/ProseList.vue | 목록 |

View File

@@ -78,7 +78,7 @@
- 댓글 아바타 이미지 로드 실패 시 이니셜 아바타로 즉시 대체한다. - 댓글 아바타 이미지 로드 실패 시 이니셜 아바타로 즉시 대체한다.
- 공개 게시물 본문은 콘텐츠 타입별 컴포넌트로 분리해 추후 스타일 변경이 쉽도록 구성 - 공개 게시물 본문은 콘텐츠 타입별 컴포넌트로 분리해 추후 스타일 변경이 쉽도록 구성
- 인용문(`>`)은 왼쪽 세로 막대형 기본 스타일로 표시한다. 첫 줄 옵션 `> [!bg=blue]` 또는 `> {bg=blue}`는 인용 막대 색상으로 반영하며, 지원 값은 콜아웃과 같은 `gray`, `blue`, `green`, `yellow`, `red`, `purple`이다. - 인용문(`>`)은 왼쪽 세로 막대형 기본 스타일로 표시한다. 첫 줄 옵션 `> [!bg=blue]` 또는 `> {bg=blue}`는 인용 막대 색상으로 반영하며, 지원 값은 콜아웃과 같은 `gray`, `blue`, `green`, `yellow`, `red`, `purple`이다.
- 관리자 Markdown-first 글쓰기의 오른쪽 블록 설정 패널은 인용·콜아웃·코드 블록·토글 설정을 지원한다. 콜아웃은 제목·아이콘 표시 여부·아이콘·배경색, 코드 블록은 언어·줄번호 표시 여부, 토글은 기본 펼침·닫힘 상태를 선언 줄에 저장한다. 콜아웃 아이콘은 라이브·공개 화면 모두 왼쪽 상단에 배치하고, 아이콘·제목 헤더 아래에 본문을 줄바꿈해 표시한다. 아이콘 미사용 시 자리 표시자를 남기지 않는다. 라이브 문단에서는 ``` Enter로 코드 블록, `!!!` Enter로 콜아웃을 만들 수 있고, 소스·라이브 모드 모두 `Cmd/Ctrl+K`로 링크 마크다운을 삽입한다. 한글 등 IME 조합 입력 중에는 줄바꿈 직후 블록 판별이 일시적으로 비어도 마지막 블록 컨텍스트를 유지해 설정 패널이 닫히지 않게 한다. - 관리자 Markdown-first 글쓰기의 오른쪽 블록 설정 패널은 인용·콜아웃·코드 블록·토글 설정을 지원한다. 콜아웃은 제목·아이콘 표시 여부·아이콘·배경색, 코드 블록은 언어·줄번호 표시 여부, 토글은 기본 펼침·닫힘 상태를 선언 줄에 저장한다. 콜아웃 아이콘은 라이브·공개 화면 모두 왼쪽 상단에 배치하고, 아이콘·제목 헤더 아래에 본문을 줄바꿈해 표시한다. 아이콘 미사용 시 자리 표시자를 남기지 않는다. 라이브 문단에서는 `>` 입력으로 인용, ``` Enter로 코드 블록, `!!!` Enter로 콜아웃을 만들 수 있고, 소스·라이브 모드 모두 `Cmd/Ctrl+K`로 링크 마크다운을 삽입한다. 한글 등 IME 조합 입력 중에는 줄바꿈 직후 블록 판별이 일시적으로 비어도 마지막 블록 컨텍스트를 유지해 설정 패널이 닫히지 않게 한다.
- 게시물 상세의 오른쪽 사이드바는 데스크톱에서 추천 사이트 대신 본문 H1~H3 제목 기반 TOC를 표시한다. TOC 링크는 본문 제목에 부여된 앵커 ID로 부드럽게 이동하며, 고정 상단 헤더 높이와 여백을 반영해 제목이 화면 밖에 걸리지 않게 한다. 본문 스크롤 중에는 현재 제목에 해당하는 TOC 항목을 강조하고, 목차 항목이 많으면 TOC 내부 스크롤이 활성 항목을 따라간다. 본문 제목이 없으면 목차 없음 문구를 표시한다. 오른쪽 사이드바가 본문 아래로 내려가는 모바일 폭에서는 TOC를 숨긴다. - 게시물 상세의 오른쪽 사이드바는 데스크톱에서 추천 사이트 대신 본문 H1~H3 제목 기반 TOC를 표시한다. TOC 링크는 본문 제목에 부여된 앵커 ID로 부드럽게 이동하며, 고정 상단 헤더 높이와 여백을 반영해 제목이 화면 밖에 걸리지 않게 한다. 본문 스크롤 중에는 현재 제목에 해당하는 TOC 항목을 강조하고, 목차 항목이 많으면 TOC 내부 스크롤이 활성 항목을 따라간다. 본문 제목이 없으면 목차 없음 문구를 표시한다. 오른쪽 사이드바가 본문 아래로 내려가는 모바일 폭에서는 TOC를 숨긴다.
- 제목 우측 공유 버튼을 누르면 게시물 공유 모달을 연다. - 제목 우측 공유 버튼을 누르면 게시물 공유 모달을 연다.
- 로그인 회원 ID가 게시물 `author_id`와 같으면 제목 우측 공유 버튼 옆에 수정 아이콘을 표시하며, 클릭 시 `/admin/posts/:id` 편집 화면을 새 탭으로 연다. - 로그인 회원 ID가 게시물 `author_id`와 같으면 제목 우측 공유 버튼 옆에 수정 아이콘을 표시하며, 클릭 시 `/admin/posts/:id` 편집 화면을 새 탭으로 연다.

View File

@@ -1,5 +1,13 @@
# 업데이트 이력 # 업데이트 이력
## v1.5.57
- 게시물 글쓰기: 라이브 모드 인용을 줄 단위 편집 대신 단일 멀티라인 편집 영역으로 변경.
- 게시물 글쓰기: 라이브 문단에서 `>` 입력 시 인용 블록으로 즉시 전환되도록 추가.
- 게시물 글쓰기: 라이브 코드 블록 본문 입력 중 줄 번호와 원본 마크다운이 즉시 동기화되도록 수정.
- 게시물 글쓰기: 라이브 콜아웃 본문 입력 중 원본 마크다운이 즉시 동기화되도록 수정.
- 게시물 글쓰기: 콜아웃 등 멀티라인 블록 내부 두 번째 줄 이후 맨 앞 Backspace가 본문을 중복 병합하던 문제 수정.
## v1.5.56 ## v1.5.56
- 게시물 글쓰기: 소스 모드 코드·콜아웃·토글 선언 줄에서 라이브 모드로 전환해도 실제 본문 줄로 포커스가 복원되도록 수정. - 게시물 글쓰기: 소스 모드 코드·콜아웃·토글 선언 줄에서 라이브 모드로 전환해도 실제 본문 줄로 포커스가 복원되도록 수정.

4
package-lock.json generated
View File

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

View File

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