v1.4.2: 라이브 이미지·갤러리 편집 UX와 공개 화면 색상 정리

라이브 모드 이미지·갤러리 드래그 병합·분리, 갤러리 개별 편집, 블록 패널 유지, 다크모드 인용·사이드바·리스트 마커 색상을 보정한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-21 17:07:52 +09:00
parent 095a8fa5f0
commit 6919669330
14 changed files with 1551 additions and 91 deletions

View File

@@ -46,7 +46,7 @@
- 라이트 모드 기본 배경은 `#fcfcfc`로 통일하고 패널 구분은 보더로 처리
- 시스템 다크 모드는 `prefers-color-scheme: dark` 기준으로 우선 대응
- 사용자 수동 테마 전환은 `html[data-theme]``localStorage.SITE_THEME`로 관리한다. 첫 페인트 전 `lib/site-theme-init.js` 인라인 스크립트가 테마를 적용해 시스템 다크·저장 라이트 불일치 시 깜빡임을 줄인다. 공개 페이지 로딩 중에는 `#site-splash`에 캐시된 로고 이미지 URL(`SITE_BRAND_LOGO_URL`, localStorage) 또는 사이트 제목(`NUXT_PUBLIC_SITE_TITLE`)을 잠깐 표시하고, 앱 마운트 후 `site-app-ready`로 숨긴다. `site_settings.logo_text`(기본 `井`)는 **이미지 로고가 없을 때** 헤더·사이드바에 쓰는 짧은 기호이며 localStorage·스플래시와는 별개다.
- Thred 참고 화면처럼 사이드바와 본문은 같은 화면 안에서 구분되는 패널과 라인으로 표현
- Thred 참고 화면처럼 사이드바와 본문은 같은 화면 안에서 라인으로 구분한다. 사이드바 자체 배경은 라이트/다크 모두 기본 화면 배경(`--site-bg`)과 통일하고, 내부 카드형 요소만 패널 배경을 사용한다.
### 홈 Featured (인덱스)
@@ -170,11 +170,11 @@ components/content/
- 리스트
- Unordered: `- 항목`
- Ordered: `1. 항목`
- 렌더링: `ProseList.vue` (마커 컬러, 간격, 줄높이 통일)
- 렌더링: `ProseList.vue` (마커 컬러는 글쓰기 화면과 같은 파란 계열, 간격, 줄높이 통일)
- 인용구
- 기본: `> 한 줄` 또는 `>` 연속 여러 줄(멀티라인)
- 대체 스타일(Alternative): `>>>`로 시작해 `<<<`로 끝나는 블록
- 렌더링: `ProseBlockquote.vue` (`variant=default|alt`)
- 렌더링: `ProseBlockquote.vue` (`variant=default|alt`, 기본 인용은 다크모드에서도 밝은 배경 위 어두운 텍스트 유지)
- 이미지
- 기본: `![](url)` — 이미지 아래 캡션 없음
- 캡션(표시용): `![](url "캡션")` — 따옴표 안 문자열만 `ProseImage` figcaption으로 표시
@@ -183,7 +183,8 @@ components/content/
- 렌더링: `ProseImage.vue` (라운드/보더/패널 배경)
- 이미지 갤러리
- `:::gallery` ~ `:::` fenced block 내부에 이미지 마크다운 행을 여러 개 작성
- 렌더링: `ContentMarkdownRenderer.vue` (그리드 + 라이트박스, Esc 닫기·←/→ 이전·다음)
- 렌더링: `ContentMarkdownRenderer.vue` (최대 3개 단위 행 + 라이트박스, Esc 닫기·←/→ 이전·다음)
- 갤러리 행은 1개일 때 전체 폭, 2~3개일 때 행 전체 폭을 나눠 쓰며 이미지 로드 후 자연 비율(가로/세로)에 따라 셀 너비를 조정한다.
- 비디오·오디오·파일 카드
- 비디오: `:::video` ~ `:::` (`url`, `title`, `poster`, `caption` 키값 또는 URL 단독 줄)
- 오디오: `:::audio` ~ `:::` (`url`, `title`, `description`)
@@ -520,12 +521,17 @@ components/content/
- `Cmd/Ctrl+E`는 작성 모드와 미리보기 모드를 전환하며, 미리보기에서 작성 모드로 돌아올 때 기존 커서 위치와 textarea 스크롤을 복원한다.
- 관리자 미리보기 패널은 공개 렌더러를 쓰되 밝은 관리자 배경 기준의 본문 색상 변수를 별도로 지정한다.
- 관리자 미리보기에서 `ContentMarkdownRenderer``interactive`를 켠다. 갤러리 이미지는 드래그로 순서를 바꿀 수 있으며, 드래그 중 다른 셀 위에 올리면 해당 셀에 주황 테두리와 「여기로 이동」 표시로 드롭 위치를 보여 준 뒤 `gallery-reorder`로 마크다운을 갱신한다.
- 라이브 모드 단일 이미지 블록은 드래그 가능하다. `![](...)` 이미지 줄과 단독 이미지 URL 줄 모두 같은 이미지 블록으로 다룬다. 다른 이미지 블록 위에 드롭하면 두 줄을 `:::gallery` fenced block 한 개로 병합하며(`merge-images-to-gallery`), 문서 순서를 유지해 위쪽 이미지가 먼저 들어간다. 자동 인접 병합은 하지 않는다.
- 라이브 모드 갤러리 블록은 이미지 블록과 같은 선택형 카드로 취급한다. Tab/클릭으로 포커스할 수 있고, 포커스 상태에서 방향키 위/아래 이동을 지원한다. 갤러리 이미지 hover/focus 시 개별 편집/삭제 버튼을 제공하며, 편집 버튼은 해당 이미지 줄 기준으로 갤러리 블록 설정 패널을 연다.
- 라이브 모드 단일 이미지 블록을 기존 갤러리 이미지 셀에 드롭하면 해당 셀 뒤에 이미지를 추가하고 원래 단일 이미지 줄은 제거한다(`insert-image-to-gallery`).
- 라이브 모드 갤러리 이미지를 블록 사이 얇은 삽입선(또는 문서 맨 아래 삽입선)에 드롭하면 해당 위치에 단일 이미지 마크다운 줄을 삽입하고 갤러리에서 제거한다(`extract-gallery-image`). 갤러리에 이미지가 1장만 남으면 갤러리 블록을 단일 이미지 줄로 바꾸고, 0장이면 갤러리 블록을 제거한다.
- `ProseImage`는 URL이 비어 있거나 로드에 실패해도 최소 높이 placeholder와 「이미지를 불러올 수 없음」 안내를 표시해 라이브 모드에서 블록 선택·편집이 가능하다.
- 관리자 **라이브 모드**(미리보기) 인라인 편집: 문단·빈 줄·제목·인용·목록·코드 블록·콜아웃·토글을 렌더 스타일 그대로 contenteditable로 수정한다. **Enter**·**Shift+Enter** 모두 다음 문단(블록) 분리. 문단 안 `/`로 슬래시 명령 메뉴(`/image`+Enter 이미지 삽입 등). **소스(작성) 모드** textarea에서도 동일한 `/` 슬래시 메뉴를 사용하며, 상단 마크다운 툴바는 두지 않는다. 슬래시 기본 제목은 **h2·h3·h4**만 표시하며, 본문 **h1**은 `/h1` 검색 시에만 삽입한다(게시물 **제목 필드**가 페이지의 유일한 h1). 콜아웃 옵션은 첫 줄 `:::callout emoji=💡 bg=blue`처럼 `emoji`·`bg`(gray|blue|green|yellow|red|purple|pink)로 지정하며, 라이브 모드에서는 아이콘 클릭으로 모달에서 편집한다(이모지 7종 프리셋·배경색 스와치, 직접 입력 없음). 코드 블록은 ` ```언어`·`nolinenos`(줄 번호 숨김)를 지원한다. 라이브·공개 모두 `ProseCodeBlock`(`#15171a`, `px-4 py-3`, `text-sm leading-6`)으로 동일하게 표시한다. 라이브 모드 호버·포커스 시 Language 입력·줄번호 토글이 보인다. 공개 화면에는 언어 라벨 옆 **복사** 버튼으로 본문을 클립보드에 넣는다. 본문 하단 클릭으로 새 문단을 추가한다.
- 이미지 파일을 붙여넣거나 드롭하면 관리자 업로드 API로 저장한 뒤 현재 커서 위치에 이미지 또는 갤러리 마크다운을 삽입한다.
- 툴바 `이미지`·`갤러리`는 미디어 모달을 연다. 모달 기본 탭은 **미디어 라이브러리**이며 **업로드** 탭에서 드래그·파일 선택 후 즉시 삽입한다.
- 미디어 라이브러리에서 단일 이미지를 선택하면 `![alt](url)` 형식으로 삽입한다.
- 미디어 라이브러리에서 여러 이미지를 선택하면 `:::gallery` fenced block으로 삽입한다.
- 작성 모드에서 커서가 이미지 마크다운 줄, `:::gallery`, 단독 URL 임베드 줄 또는 기존 `:::embed` 블록 안에 있고 textarea(또는 블록 패널)에 포커스가 있으면 게시물 설정 사이드바(420px) 위에 **블록 설정 패널**(`AdminEditorBlockPanel`)이 오른쪽에서 슬라이드 인한다. 본문 포커스가 완전히 이탈하면 슬라이드 아웃한다.
- 작성 모드에서 커서가 이미지 마크다운 줄, `:::gallery`, 단독 URL 임베드 줄 또는 기존 `:::embed` 블록 안에 있고 textarea(또는 블록 패널)에 포커스가 있으면 게시물 설정 사이드바(420px) 위에 **블록 설정 패널**(`AdminEditorBlockPanel`)이 오른쪽에서 슬라이드 인한다. 본문·패널 바깥을 클릭하면 슬라이드 아웃한다. 갤러리 이미지 추가 미디어 모달을 여는 동안에는 활성 갤러리 컨텍스트와 패널 상태를 유지한다.
- 블록 설정 패널: 이미지·갤러리(캡션, **파일명을 캡션으로 사용** 토글·기본 끔, URL, 갤러리 순서·삭제·추가), 임베드(URL). `AdminMarkdownEditor``block-panel` 이벤트로 상태를 `AdminPostForm`에 전달한다.
- 미디어 라이브러리 갤러리 다중 선택 시 선택 항목은 **주황(`#ff7a00`) 굵은 테두리**로 표시한다.
- 옵시디언식 토큰 숨김/백스페이스 복원 Live Preview는 후속 단계로 둔다.
@@ -571,8 +577,9 @@ components/content/
- 저장된 태그가 없는 게시물에는 상세 메타 행·홈 Latest·`/posts` 목록 카드에 태그 배지나 더미 라벨을 표시하지 않는다.
- 검색엔진 노출 제외가 켜진 글은 robots 메타를 `noindex, nofollow`로 출력한다.
- 공개 상세 화면의 `og:image`와 Twitter large image 카드는 대표 이미지를 기본값으로 사용한다.
- 이미지 블록은 관리자 업로드 API로 이미지를 업로드하고 `![](url)` 또는 파일명 캡션 토글 시 `![](url "파일명")` 형식으로 저장한다.
- 이미지 블록은 관리자 업로드 API로 이미지를 업로드하고 `![](url)` 또는 파일명 캡션 토글 시 `![](url "파일명")` 형식으로 저장한다. 단독 이미지 파일 URL(`jpg`, `png`, `webp`, `gif`, `avif`, `svg`) 한 줄은 임베드가 아니라 이미지 블록으로 렌더링한다.
- 이미지/갤러리 삽입 시 캡션은 기본 비우며, 블록 설정 패널에서 **파일명을 캡션으로 사용** 토글로 이미지 아래에 URL 파일명을 표시한다.
- 라이브 모드 이미지 블록은 hover/focus 시 우측 상단에 `편집`·`삭제` 버튼을 표시한다. `편집`은 기존 오른쪽 이미지 설정 패널을 열어 이미지 URL·캡션·파일명 캡션 사용 여부를 수정한다.
- 이미지 블록 표시 옵션은 `regular`, `wide`, `full` 값을 사용하며 `regular`는 width 옵션을 생략한다.
- 갤러리 블록은 `:::gallery` fenced block 안에 이미지 마크다운 행을 여러 개 저장한다.
- 관리자 갤러리 미디어 선택은 여러 이미지를 선택한 뒤 확인 버튼으로 적용한다.
@@ -586,11 +593,14 @@ components/content/
- `emoji=none`이면 공개 렌더러에서 이모지를 숨긴다.
- 콜아웃 배경 프리셋은 `gray`, `blue`, `green`, `yellow`, `red`, `purple`, `pink`를 지원한다.
- 토글 블록은 `:::toggle 제목` fenced block 안에 펼침 본문을 저장한다. 라이브 모드에서는 제목·본문을 인라인 편집하며, chevron으로 펼침·접힘 시 본문이 애니메이션된다.
- 임베드 블록은 단독 `http(s)` URL 한 줄을 기본 저장 형식으로 사용한다.
- 임베드 블록은 이미지 파일 URL을 제외한 단독 `http(s)` URL 한 줄을 기본 저장 형식으로 사용한다.
- 기존 `:::embed` fenced block은 이전 콘텐츠 호환을 위해 계속 파싱·렌더링한다.
- 관리자 Markdown-first 에디터의 라이브/스타일 모드에서 임베드 블록은 URL 입력 카드 없이 즉시 실제 임베드 프리뷰로 표시된다. 임베드·비디오·오디오·파일 프리뷰 카드는 hover/focus 시 우측 상단 삭제 버튼을 표시한다. 블록 래퍼에 포커스한 상태에서 `Backspace`·`Delete`·`Ctrl/Cmd+Shift+K`로 삭제하고, `Enter`로 아래 빈 줄을 추가하며, `ArrowUp`·`ArrowDown`은 브라우저 스크롤 대신 이전/다음 편집 줄로 이동한다.
- 라이브/스타일 모드에서 제목 블록 Enter는 현재 제목 내용을 저장한 뒤 바로 아래 빈 문단을 추가하고, 원문 마크다운 편집 상태로 전환하지 않는다.
- 게시물 작성 화면 상단 제목 입력 후 Enter는 현재 에디터 모드를 유지한 채 본문 첫 줄(마크다운 첫 줄이 제목이면 그 다음 줄)로 포커스를 옮긴다. 라이브 모드 전환 시 미리보기 스크롤은 맨 위에서 시작한다.
- 게시물 작성 화면 상단 제목 입력 후 Enter는 현재 에디터 모드를 유지한 채 본문 첫 줄(마크다운 첫 줄이 제목이면 그 다음 줄)로 포커스를 옮긴다.
- 소스 모드 라인 번호는 논리 줄 수를 표시하되, 긴 문장 자동 줄바꿈으로 textarea의 한 줄 높이가 늘어나면 라인 번호 칸도 같은 높이로 맞춘다.
- 소스 모드에서 라이브 모드로 전환하면 현재 textarea 커서 줄과 줄 안 오프셋을 기준으로 대응하는 라이브 편집 블록에 포커스를 둔다. 이때 현재 화면 위치를 불필요하게 맨 위나 맨 아래로 이동하지 않는다.
- 라이브 모드에서 소스 모드로 전환하면 현재 포커스된 블록 또는 화면 상단에 가까운 원본 줄을 기준으로 textarea 커서와 스크롤 위치를 복원한다.
- YouTube 임베드 URL은 공개 화면에서 본문 폭 기준 16:9 iframe으로 렌더링한다.
- Twitter/X 게시물 URL(`twitter.com`·`x.com`·`mobile.twitter.com`, 경로에 `status` 포함)은 `platform.twitter.com/embed/Tweet.html` iframe으로 렌더링하며, 테마는 `useThemeMode()`와 동기화한다. X 공식 iframe의 내부 최대 폭 때문에 공개 화면에서는 카드 폭을 좁혀 중앙 정렬한다.
- Mastodon 공개 게시물 URL(`/@user/id`, `/users/user/statuses/id`)은 `{원본 URL}/embed` iframe으로 렌더링한다. iframe 로드 후 Mastodon 공식 embed 방식과 같은 `postMessage` 높이 요청을 보내 응답 높이를 반영한다. 인스턴스가 embed를 차단하거나 지원하지 않으면 브라우저 iframe 정책에 따라 표시되지 않을 수 있다.