From 675e6bca783b3745a6a57d0b13b9cc76261c2276 Mon Sep 17 00:00:00 2001 From: zenn Date: Thu, 4 Jun 2026 11:09:40 +0900 Subject: [PATCH] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EC=97=90?= =?UTF-8?q?=EB=94=94=ED=84=B0=20=EC=BD=9C=EC=95=84=EC=9B=83=20=EC=A4=84=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../content/ContentMarkdownCalloutEditor.vue | 15 ++++- .../ContentMarkdownCodeBlockEditor.vue | 3 +- .../content/ContentMarkdownEditableInline.vue | 8 ++- .../content/ContentMarkdownRenderer.vue | 58 +++++++++++++++++++ .../content/ContentMarkdownToggleEditor.vue | 3 +- docs/changelog.md | 6 ++ docs/deploy.md | 10 +++- docs/map.md | 4 +- docs/spec.md | 2 +- docs/update.md | 7 +++ package-lock.json | 4 +- package.json | 2 +- 12 files changed, 111 insertions(+), 11 deletions(-) diff --git a/components/content/ContentMarkdownCalloutEditor.vue b/components/content/ContentMarkdownCalloutEditor.vue index 4ad224f..bb27005 100644 --- a/components/content/ContentMarkdownCalloutEditor.vue +++ b/components/content/ContentMarkdownCalloutEditor.vue @@ -33,7 +33,7 @@ const props = defineProps({ } }) -const emit = defineEmits(['commit']) +const emit = defineEmits(['commit', 'delete-line', 'insert-below', 'merge-with-previous', 'leave-block']) /** * 콜아웃 마크다운 줄을 반영한다. @@ -62,6 +62,15 @@ const commitCallout = (body) => { const onBodyCommit = (body) => { commitCallout(body) } + +/** + * 콜아웃 아래로 이탈한다. + * @param {Object} payload - 이탈 페이로드 + * @returns {void} + */ +const onExitBelow = (payload) => { + emit('insert-below', payload) +} diff --git a/components/content/ContentMarkdownEditableInline.vue b/components/content/ContentMarkdownEditableInline.vue index 53ae2fc..15012d8 100644 --- a/components/content/ContentMarkdownEditableInline.vue +++ b/components/content/ContentMarkdownEditableInline.vue @@ -736,9 +736,14 @@ const onKeydown = (event) => { if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key.toLowerCase() === 'k') { if (props.sourceLine !== null) { + const lineContext = getCaretLineContext() + const sourceLine = resolvedEnterMode.value === 'multiline' + ? props.sourceLine + lineContext.lineIndex + : props.sourceLine + event.preventDefault() event.stopPropagation() - emit('delete-line', props.sourceLine) + emit('delete-line', sourceLine) } return @@ -861,6 +866,7 @@ const onKeydown = (event) => { if (event.key === 'Enter' && !event.shiftKey && parseSlashInput(readEditorValue())) { event.preventDefault() event.stopPropagation() + event.stopImmediatePropagation?.() emit('slash-apply', { sourceLine: props.sourceLine, value: readEditorValue() diff --git a/components/content/ContentMarkdownRenderer.vue b/components/content/ContentMarkdownRenderer.vue index 764f929..e8af7d0 100644 --- a/components/content/ContentMarkdownRenderer.vue +++ b/components/content/ContentMarkdownRenderer.vue @@ -1271,6 +1271,26 @@ const onCalloutBlockCommit = (block, replacementLines) => { commitInlineBlockLines(block, replacementLines) } +/** + * 콜아웃 마지막 줄에서 아래로 이탈한다. + * @param {Object} block - 콜아웃 블록 + * @param {string|Object} payload - insert-below 페이로드 + * @returns {void} + */ +const onCalloutBlockInsertBelow = (block, payload) => { + const { value } = normalizeInsertBelowPayload(payload) + const openingLine = getMarkdownLine(block.meta.startLine) + + onCalloutBlockCommit(block, [ + openingLine, + ...String(value ?? '').replace(/\r/g, '').split('\n'), + ':::' + ]) + pendingFocusPosition.value = 'start' + pendingFocusOffset.value = 0 + onInsertBelowBlock(block, { lines: [''] }) +} + /** * 토글 편집 반영 * @param {Object} block - 토글 블록 @@ -1782,6 +1802,39 @@ const onDeleteLine = (lineIndex) => { return } + const fencedBlock = blocks.value.find((block) => { + const startLine = block.meta?.startLine + const endLine = block.meta?.endLine + + return ['callout', 'code', 'toggle'].includes(block.type) + && typeof startLine === 'number' + && typeof endLine === 'number' + && lineIndex > startLine + && lineIndex < endLine + }) + + if (fencedBlock) { + const startLine = fencedBlock.meta.startLine + const endLine = fencedBlock.meta.endLine + const bodyLineCount = Math.max(0, endLine - startLine - 1) + + if (bodyLineCount <= 1) { + pendingFocusLine.value = startLine > 0 ? startLine - 1 : 0 + pendingFocusPosition.value = startLine > 0 ? 'end' : 'start' + emit('block-content-change', { + startLine, + endLine, + replacementLines: [] + }) + return + } + + pendingFocusLine.value = lineIndex > startLine + 1 ? lineIndex - 1 : lineIndex + pendingFocusPosition.value = lineIndex > startLine + 1 ? 'end' : 'start' + emit('delete-line', lineIndex) + return + } + const focusLine = lineIndex > 0 ? lineIndex - 1 : 0 pendingFocusLine.value = focusLine @@ -2555,6 +2608,9 @@ onBeforeUnmount(() => { :body-source-line="block.meta.startLine + 1" :model-value="block.text" @commit="onCalloutBlockCommit(block, $event)" + @delete-line="onDeleteLine" + @insert-below="onCalloutBlockInsertBelow(block, $event)" + @merge-with-previous="onMergeWithPreviousLine(block.meta.startLine + 1, $event)" /> { :model-value="block.text" @commit="onToggleBlockCommit(block, $event)" @insert-below="onToggleBlockInsertBelow(block, $event)" + @delete-line="onDeleteLine" /> diff --git a/docs/changelog.md b/docs/changelog.md index b4e4c52..b5a27ab 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # 업데이트 요약 +## v1.5.49 + +- 라이브 작성 모드에서 코드·콜아웃·토글 내부 `Cmd+Shift+K` 줄 삭제가 동작하도록 수정했다. +- 콜아웃 내부 줄 삭제와 아래 방향키 이탈 동작을 보강했다. +- `/콜아웃` Enter 생성 시 한글 조합 잔여 문자가 남을 가능성을 줄였다. + ## v1.5.48 - 게시물 작성 화면 상단의 상태 표시는 텍스트만 남기고, 게시물 보기 링크는 오른쪽 `View Post` 기능으로 통일했다. diff --git a/docs/deploy.md b/docs/deploy.md index b372270..c0fd14b 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -1,6 +1,6 @@ # 배포 가이드 -> 로컬 기준 v1.5.48에서 `npm run lint`, `npm run build` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다. +> 로컬 기준 v1.5.49에서 `npm run lint`, `npm run build` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다. ## 빌드 유형 @@ -68,6 +68,14 @@ docker exec sori-studio-db pg_isready -U sori_studio -d sori_studio docker exec sori-studio-db psql -U sori_studio -d sori_studio -c 'SELECT count(*) AS posts_count FROM posts;' ``` +### v1.5.49 참고 + +- 추가 DB 마이그레이션은 없다. +- 관리자 글쓰기 라이브 모드에서 코드·콜아웃·토글 내부 커서 위치별 `Cmd+Shift+K` 줄 삭제를 확인한다. +- 코드·콜아웃·토글 본문 마지막 1줄에서 `Cmd+Shift+K` 입력 시 블록 전체가 삭제되는지 확인한다. +- 라이브 콜아웃 마지막 줄에서 아래 방향키로 다음 블록으로 빠져나가는지 확인한다. +- `/콜아웃` Enter 생성 후 콜아웃 본문에 마지막 조합 글자가 남지 않는지 확인한다. + ### v1.5.48 참고 - 추가 DB 마이그레이션은 없다. diff --git a/docs/map.md b/docs/map.md index d98b894..4ececf8 100644 --- a/docs/map.md +++ b/docs/map.md @@ -93,7 +93,7 @@ | components/admin/AdminMediaVideoThumbnail.vue | 관리자 미디어 목록 비디오 항목의 초반 프레임 캔버스 썸네일 | | components/admin/AdminPostForm.vue | 관리자 글 작성/수정 폼, Ghost형 툴바(왼쪽 상태 텍스트·Publish/Update/Unpublish/Unschedule, 서버 반영 상태 기준 분기), 초안만 서버 디바운스 자동 저장·신규 임시 슬러그·발행·예약·멤버십·비공개 상태 저장, 발행 모달(중앙 배치), 좌우 설정 패널(작은 화면은 오른쪽 고정 오버레이), 오른쪽 `View Post` 링크, 미리보기 emit·미저장 이탈 가드, 추천 글 토글, 태그 색상 배지 다중 입력·메인 태그 드롭다운·부분 검색 추천 | | components/admin/AdminPageForm.vue | 관리자 페이지 작성/수정 폼, 게시글 작성과 같은 전체 화면 에디터·상단 저장 툴바·접이식 오른쪽 설정 패널, 페이지 공개 상태 선택, HTML 문서 기본 모드, 빈 본문/`!`+Tab HTML 골격 자동 완성, 항상 보이는 일반 텍스트/HTML 모드 선택, 한글 제목 영문 슬러그 자동 변환, HTML textarea 커서 위치 파일 URL 삽입 | -| components/admin/AdminMarkdownEditor.vue | 관리자 글 Markdown-first 에디터, 라이브·소스 모드 `/` 슬래시 명령·미디어 모달(이미지·갤러리·비디오·오디오·파일), 커서 블록 컨텍스트·`block-panel` emit, 라이브 이미지 설정 패널·이미지↔갤러리 드래그 변환(`merge-images-to-gallery`·`insert-image-to-gallery`·`extract-gallery-image`), 블록 패널 바깥 클릭 닫기·미디어 모달 중 유지, 인용·콜아웃·코드·토글 선언 줄 옵션 수정, IME 조합 중 블록 패널 유지, 소스 모드 wrap 라인 번호 보정·라이브↔소스 위치 복원 | +| components/admin/AdminMarkdownEditor.vue | 관리자 글 Markdown-first 에디터, 라이브·소스 모드 `/` 슬래시 명령·미디어 모달(이미지·갤러리·비디오·오디오·파일), 커서 블록 컨텍스트·`block-panel` emit, 라이브 이미지 설정 패널·이미지↔갤러리 드래그 변환(`merge-images-to-gallery`·`insert-image-to-gallery`·`extract-gallery-image`), 코드·콜아웃·토글 내부 줄 삭제, 블록 패널 바깥 클릭 닫기·미디어 모달 중 유지, 인용·콜아웃·코드·토글 선언 줄 옵션 수정, IME 조합 중 블록 패널 유지, 소스 모드 wrap 라인 번호 보정·라이브↔소스 위치 복원 | | components/admin/AdminEditorBlockPanel.vue | 게시물 설정 사이드바 오버레이 블록 설정(이미지·갤러리·임베드·인용 배경색·콜아웃·코드·토글), 갤러리 선택 이미지 강조 | | components/admin/AdminBlockEditor.vue | 관리자 글 블록형 에디터, 이미지/갤러리/콜아웃/토글/임베드 블록, 콜아웃 Emoji on/off·이모지 프리셋·인용과 같은 배경 프리셋 선택(우측 고정 설정 패널), 갤러리 복수 미디어 선택·이미지 수별 열 배치·삽입 위치 표시 드래그 순서 변경, 한글 조합 입력 처리, Shift+Enter 줄바꿈, 코드 블록 단축 변환, AFFiNE 참고 세로 막대형 블록 핸들 선택/삭제/드래그 이동과 삽입선 표시, 하단 빈 입력 블록 유지, 본문 placeholder 표시 | | components/admin/AdminTagForm.vue | 관리자 태그 생성/수정 폼(이름/슬러그/설명/색상만 편집) | @@ -111,7 +111,7 @@ | 파일 | 화면 위치 | |------|-----------| | components/content/ContentRenderer.vue | 게시물/페이지 본문 | -| components/content/ContentMarkdownRenderer.vue | 마크다운 문자열 기반 본문 렌더링, 문단 text-base(16px), 빈 줄 spacer 보존·hard break `
` 처리, 확장 블록 파싱, 인용 배경 옵션(`> [!bg=...]`), 라이브 콜아웃·인용 포커스 기반 오른쪽 설정 패널 연결, 라이브 방향키 이동 시 편집 가능한 줄·카드형 블록 탐색, 라이브 이미지·갤러리 드래그 병합·추가·분리 UI, 갤러리 비율 기반 행 레이아웃, 라이브 갤러리 개별 이미지 편집·삭제, 리스트 마커 파란 계열 통일 | +| components/content/ContentMarkdownRenderer.vue | 마크다운 문자열 기반 본문 렌더링, 문단 text-base(16px), 빈 줄 spacer 보존·hard break `
` 처리, 확장 블록 파싱, 인용 배경 옵션(`> [!bg=...]`), 라이브 콜아웃·인용 포커스 기반 오른쪽 설정 패널 연결, 라이브 방향키 이동 시 편집 가능한 줄·카드형 블록 탐색, 라이브 코드·콜아웃·토글 내부 줄 삭제와 마지막 줄 블록 삭제, 라이브 이미지·갤러리 드래그 병합·추가·분리 UI, 갤러리 비율 기반 행 레이아웃, 라이브 갤러리 개별 이미지 편집·삭제, 리스트 마커 파란 계열 통일 | | components/content/ProseHeading.vue | h1~h6 제목, 기본 mt-12 제거 | | components/content/ProseImage.vue | 본문 내 이미지, 로드 실패·빈 URL placeholder | | components/content/ProseList.vue | 목록 | diff --git a/docs/spec.md b/docs/spec.md index 8fb4020..ccabb81 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -627,7 +627,7 @@ components/content/ - 라이브 모드 갤러리 이미지를 블록 사이 얇은 삽입선(또는 문서 맨 아래 삽입선)에 드롭하면 해당 위치에 단일 이미지 마크다운 줄을 삽입하고 갤러리에서 제거한다(`extract-gallery-image`). 갤러리에 이미지가 1장만 남으면 갤러리 블록을 단일 이미지 줄로 바꾸고, 0장이면 갤러리 블록을 제거한다. - `ProseImage`는 URL이 비어 있거나 로드에 실패해도 최소 높이 placeholder와 「이미지를 불러올 수 없음」 안내를 표시해 라이브 모드에서 블록 선택·편집이 가능하다. - 인용(`>`) 블록은 첫 인용 줄에 `> [!bg=yellow]` 또는 `> {bg=yellow}` 옵션 줄을 두면 해당 줄은 숨기고 블록 배경을 바꾼다. 지원 배경 프리셋은 콜아웃과 같은 `gray`, `blue`, `green`, `yellow`, `red`, `purple`이며, 옵션이 없으면 회색 기본 인용 스타일을 쓴다. -- 관리자 **라이브 모드**(미리보기) 인라인 편집: 문단·빈 줄·제목·인용·목록·코드 블록·콜아웃·토글을 렌더 스타일 그대로 contenteditable로 수정한다. blur·문단 이동(방향키) 시 편집 영역의 ``·`` 등을 `**`·`*` 마크다운으로 다시 직렬화해 저장한다. **Enter**·**Shift+Enter** 모두 다음 문단(블록) 분리. 문단 안 `/`로 슬래시 명령 메뉴(`/image`+Enter 이미지 삽입 등). **소스(작성) 모드** textarea에서도 동일한 `/` 슬래시 메뉴를 사용하며, 상단 마크다운 툴바는 두지 않는다. 슬래시 기본 제목은 **h2·h3·h4**만 표시하며, 본문 **h1**은 `/h1` 검색 시에만 삽입한다(게시물 **제목 필드**가 페이지의 유일한 h1). 콜아웃 옵션은 첫 줄 `:::callout emoji=💡 bg=blue`처럼 `emoji`·`bg`(gray|blue|green|yellow|red|purple)로 지정하며, 라이브 모드에서는 블록에 포커스가 들어오면 오른쪽 설정 패널에서 수정한다. 코드 블록은 ` ```언어`·`nolinenos`(줄 번호 숨김)를 지원한다. 라이브·공개 모두 `ProseCodeBlock`(`#15171a`, `px-4 py-3`, `text-sm leading-6`)으로 동일하게 표시한다. 라이브 모드 호버·포커스 시 Language 입력·줄번호 토글이 보인다. 공개 화면에는 언어 라벨 옆 **복사** 버튼으로 본문을 클립보드에 넣는다. 본문 하단 클릭으로 새 문단을 추가한다. +- 관리자 **라이브 모드**(미리보기) 인라인 편집: 문단·빈 줄·제목·인용·목록·코드 블록·콜아웃·토글을 렌더 스타일 그대로 contenteditable로 수정한다. blur·문단 이동(방향키) 시 편집 영역의 ``·`` 등을 `**`·`*` 마크다운으로 다시 직렬화해 저장한다. **Enter**·**Shift+Enter** 모두 다음 문단(블록) 분리. 문단 안 `/`로 슬래시 명령 메뉴(`/image`+Enter 이미지 삽입 등). **소스(작성) 모드** textarea에서도 동일한 `/` 슬래시 메뉴를 사용하며, 상단 마크다운 툴바는 두지 않는다. 슬래시 기본 제목은 **h2·h3·h4**만 표시하며, 본문 **h1**은 `/h1` 검색 시에만 삽입한다(게시물 **제목 필드**가 페이지의 유일한 h1). `Cmd+Shift+K`는 현재 줄을 삭제하며 코드·콜아웃·토글 블록 내부에서는 커서가 있는 본문 줄을 삭제하고, 남은 본문 줄이 1개뿐이면 fenced 블록 전체를 삭제한다. 콜아웃 옵션은 첫 줄 `:::callout emoji=💡 bg=blue`처럼 `emoji`·`bg`(gray|blue|green|yellow|red|purple)로 지정하며, 라이브 모드에서는 블록에 포커스가 들어오면 오른쪽 설정 패널에서 수정한다. 코드 블록은 ` ```언어`·`nolinenos`(줄 번호 숨김)를 지원한다. 라이브·공개 모두 `ProseCodeBlock`(`#15171a`, `px-4 py-3`, `text-sm leading-6`)으로 동일하게 표시한다. 라이브 모드 호버·포커스 시 Language 입력·줄번호 토글이 보인다. 공개 화면에는 언어 라벨 옆 **복사** 버튼으로 본문을 클립보드에 넣는다. 본문 하단 클릭으로 새 문단을 추가한다. - 이미지 파일을 붙여넣거나 드롭하면 관리자 업로드 API로 저장한 뒤 현재 커서 위치에 이미지 또는 갤러리 마크다운을 삽입한다. - 툴바 `이미지`·`갤러리`는 미디어 모달을 연다. 모달 기본 탭은 **미디어 라이브러리**이며 **업로드** 탭에서 드래그·파일 선택 후 즉시 삽입한다. - 미디어 라이브러리에서 단일 이미지를 선택하면 `![alt](url)` 형식으로 삽입한다. diff --git a/docs/update.md b/docs/update.md index eb31036..77b39c0 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,12 @@ # 업데이트 이력 +## v1.5.49 + +- 게시물 글쓰기: 라이브 모드 `Cmd+Shift+K`가 코드·콜아웃·토글 블록 내부 현재 줄을 삭제하도록 수정. +- 게시물 글쓰기: 코드·콜아웃·토글 블록 내부 마지막 본문 줄에서 `Cmd+Shift+K`를 누르면 fenced 블록 전체를 삭제하도록 수정. +- 게시물 글쓰기: 콜아웃 라이브 편집에서 내부 줄 삭제와 마지막 줄 아래 방향키 이탈 이벤트가 부모 에디터로 전달되도록 수정. +- 게시물 글쓰기: 라이브 슬래시 명령 Enter 적용 시 키 이벤트 전파를 더 강하게 차단해 한글 IME 입력 잔여 문자가 블록 본문에 남을 가능성을 줄임. + ## v1.5.48 - 게시물 글쓰기: 상단 상태 표시에서 외부 이동 아이콘과 게시물 링크 이동 기능 제거. diff --git a/package-lock.json b/package-lock.json index 5ffe528..f794675 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sori.studio", - "version": "1.5.48", + "version": "1.5.49", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sori.studio", - "version": "1.5.48", + "version": "1.5.49", "hasInstallScript": true, "dependencies": { "@nuxtjs/tailwindcss": "^6.14.0", diff --git a/package.json b/package.json index 9e83239..c81bd8a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sori.studio", - "version": "1.5.48", + "version": "1.5.49", "private": true, "type": "module", "imports": {