본문 이미지 최적화·라이트박스·번호형 페이지네이션 반영 (v1.10.3)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-01 16:19:45 +09:00
parent 0eca6b288e
commit c686b1c848
20 changed files with 812 additions and 143 deletions

View File

@@ -1,5 +1,22 @@
# 업데이트 요약
## v1.10.3
- 게시물 목록과 태그 목록 하단 페이지네이션을 번호형 빠른 이동 UI로 바꿨다.
- 페이지가 많으면 처음·끝과 현재 페이지 주변 번호만 보여 주고 중간은 `...`으로 접는다.
## v1.10.2
- 게시물 본문 이미지도 자동으로 본문 표시용 WebP 이미지를 생성해 우선 표시한다.
- 관리자 미디어에 본문 이미지 탭을 추가해 원본·본문 표시용 이미지·카드 썸네일을 따로 관리할 수 있게 했다.
- 미디어 라이브러리에서 중복으로 보이던 `전체` 필터 배지를 정리했다.
## v1.10.1
- 게시물 본문 일반 이미지도 클릭해서 크게 볼 수 있게 했다.
- 한 게시물 안의 일반 이미지와 갤러리 이미지를 좌우 방향키로 이어서 볼 수 있게 했다.
- 이미지 최적화와 업로드 총량 제한 등 다음 개선 과제를 정리했다.
## v1.10.0
- 글쓰기에서 Post URL 입력 직후 첫 자동 저장 중 태그나 본문을 추가하면 저장 완료 후 입력값이 사라질 수 있던 문제를 수정했다.

View File

@@ -1,6 +1,6 @@
# 배포 가이드
> 로컬 기준 v1.10.0에서 `npm run verify` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다.
> 로컬 기준 v1.10.1에서 `npm run verify` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다.
## 빌드 유형
@@ -405,6 +405,8 @@ docker compose --env-file .env.production restart sori-studio-db
| `npm run images:backfill-post-thumbnails` | 게시물 대표 이미지용 카드 썸네일(`thumbs/*-card.webp`)이 누락된 기존 업로드를 일괄 생성 |
| `npm run navigation:refresh-recommended-favicons` | 추천 사이트 파비콘 캐시를 수동으로 전체 갱신 |
본문 표시용 WebP 이미지(`thumbs/*-body.webp`)는 게시물 저장 시 생성된다. 기존 게시물의 과거 이미지는 다시 저장하거나 관리자 미디어 상세에서 개별 생성한다.
Homepage 위젯 연동은 `docs/homepage-widget.md`를 참고한다. Docker에서 Homepage가 같은 네트워크에 있으면 `http://sori-studio:3000/api/homepage-widget`을 사용한다.
---

View File

@@ -1,5 +1,17 @@
# 의사결정 이력
## 2026-07-01 v1.10.3 — 공개 목록 페이지네이션은 직접 이동을 기본으로 한다
이전/다음만 있는 페이지네이션은 글이 수십 개씩 추가되는 기간에 원하는 구간으로 이동하기 어렵다. 공개 게시물 목록과 태그 목록은 서버 페이지네이션을 이미 사용하므로, UI는 처음·끝 페이지와 현재 위치 주변 번호를 보여 주고 중간 구간은 `...`으로 접는 방식으로 바꾼다. 모바일에서는 번호가 많을 때 한 줄 가로 스크롤을 허용해 레이아웃 깨짐을 피한다.
## 2026-07-01 v1.10.2 — 본문 이미지는 원본과 표시용 파일을 분리한다
게시물 본문에 4K·고용량 이미지가 여러 장 들어가면 공개 화면 초기 로딩 비용이 커진다. 원본은 라이트박스와 보관용으로 유지하되, 본문 표시에는 `thumbs/*-body.webp` 파생 이미지를 우선 사용해 데이터 사용량을 줄인다. 관리자 미디어에서는 원본, 본문 이미지, 카드 썸네일을 탭으로 분리해 자동 생성 파일의 사용처와 삭제 제한을 쉽게 확인하게 한다.
## 2026-07-01 v1.10.1 — 게시물 이미지는 하나의 감상 흐름으로 묶는다
게시물 안에서 단일 이미지와 갤러리는 작성 구조상 다른 블록이지만, 독자는 이미지를 빠르게 이어 보는 경험을 기대한다. 따라서 라이트박스는 갤러리 단위가 아니라 게시물 본문 전체 이미지 단위로 구성하고, 클릭한 이미지 위치부터 문서 순서대로 이전·다음 탐색을 제공한다. 갤러리는 배치 표현만 담당하고, 감상 흐름은 게시물 전체 이미지 그룹이 담당한다.
## 2026-07-01 v1.10.0 — 저장 완료 기준은 요청 시점 payload로 고정한다
게시물 저장 요청이 진행되는 동안 사용자가 태그나 본문을 계속 수정할 수 있다. 성공 응답 시점의 화면 값을 그대로 저장 완료 기준으로 삼으면 실제 서버에 저장된 payload보다 최신 입력까지 저장된 것으로 오판하고, 신규 글의 첫 자동 저장 후 편집 URL로 이동하면서 최신 입력이 사라질 수 있다. 따라서 저장 완료 스냅샷은 요청에 보낸 payload로 고정하고, 저장 중 생긴 추가 변경은 저장이 끝난 뒤 다시 자동 저장되게 한다. 발행 저장 중에는 미저장 변경으로 간주해 라우트 이탈 확인을 유지한다.

View File

@@ -26,7 +26,7 @@
| 파일 | 용도 |
|------|------|
| lib/upload-size-limit.js | 관리자 미디어 업로드 종류별 최대 바이트 판별·용량 문구(서버·클라이언트 공용) |
| lib/post-card-thumbnail-url.js | 게시물 카드 썸네일 `thumbs/*-card.webp` URL 판별(대표 이미지·본문 이미지·갤러리 선택 및 서버 검증 공용) |
| lib/post-card-thumbnail-url.js | 게시물 카드 썸네일 `thumbs/*-card.webp`와 본문 표시용 이미지 `thumbs/*-body.webp` URL 판별(대표 이미지·본문 이미지·갤러리 선택 및 서버 검증 공용) |
| lib/external-favicon-url.js | 외부 URL origin 기준 Google `gstatic.com/faviconV2` 파비콘 URL 생성(내부 경로는 빈 문자열) |
| lib/navigation-url.js | 네비게이션 URL 접두사 분리·합성·정규화(`https://` 자동 보정, `/` 내부 경로) |
| lib/recommended-site-icon-url.js | 추천 사이트 아이콘 URL 우선순위(수동 썸네일 > 서버 캐시 > 외부 프록시) |
@@ -91,7 +91,7 @@
| server/routes/feed.xml.get.js | 공개 RSS 2.0 피드 별칭(`/feed.xml`) |
| server/routes/rss.get.js | 공개 RSS 2.0 피드 별칭(`/rss`) |
| server/plugins/site-custom-code.js | 공개 Nuxt HTML 응답에 사이트 설정 헤더·푸터 코드 삽입(`/admin`, `/api`, `/uploads`, `/_nuxt`, `/ads.txt` 제외) |
| server/utils/post-thumbnail-image.js | 게시물 업로드 이미지의 카드 썸네일 URL·디스크 경로·생성 가능 여부 규칙 |
| server/utils/post-thumbnail-image.js | 게시물 업로드 이미지의 카드 썸네일·본문 표시용 WebP 이미지 URL·디스크 경로·생성 가능 여부·본문 이미지 추출 규칙 |
| server/utils/sitemap.js | 공개 sitemap XML·robots.txt 생성 |
| server/utils/rss-feed.js | 공개 발행글 기반 RSS 2.0 XML 생성, 게시물 이미지 Media RSS 썸네일 출력 |
@@ -112,6 +112,7 @@
| components/site/HomeHero.vue | 홈 상단 720px 커버 배너, 라이트·다크 테마별 이미지 교체, 왼쪽 하단 오버레이 제목·본문 |
| components/site/PostCard.vue | 목록의 게시물 카드, 카드 hover 인터랙션, 태그는 있을 때만 색상 배지 메타에 표시하고 태그 페이지 링크 제공, 오른쪽 아래 게시물 링크 화살표 |
| components/site/PostCardMedia.vue | 게시물 카드 썸네일(`featuredImageThumbnail` 우선, 없으면 대표 이미지 원본 사용. 대표 이미지 없으면 모바일 3줄·`sm` 이상 4줄 제목 placeholder 표시, 이미지 유무와 관계없이 썸네일 영역 크기 고정) |
| components/site/SitePagination.vue | 공개 목록 공용 번호형 페이지네이션, 이전·다음 화살표와 첫/끝/현재 주변 페이지 직접 이동, 많은 페이지의 `...` 접기, 모바일 가로 스크롤 |
| components/site/TagHeader.vue | 태그 페이지 헤더 |
| components/comments/PostComments.vue | 게시물 상세 `#comments` 영역, 회원 댓글/답글(1단) 작성 및 목록 표시, 입력값 기반 등록 버튼 활성화, 작성자 썸네일/좋아요/상대시간 표시 |
@@ -147,12 +148,12 @@
| 파일 | 화면 위치 |
|------|-----------|
| components/content/ContentRenderer.vue | 게시물/페이지 본문 |
| components/content/ContentMarkdownRenderer.vue | 마크다운 문자열 기반 본문 렌더링, 문단 text-base(16px), 빈 줄 spacer 보존·hard break `<br>` 처리, 표준 마크다운 표 파싱·렌더링(셀 내부 인라인 링크와 `<br>` 줄바꿈 포함), 공개 게시물 인아티클 광고를 본문 길이에 따라 0~2회 삽입, Obsidian식 위첨자·아래첨자 인라인 렌더링, 확장 블록 파싱, `:::` fenced 블록 원본 범위 보정, 닫히지 않은 코드 펜스 하위 콘텐츠 보호, 인용 막대 색상 옵션(`> [!bg=...]`), 라이브 문단 `>` 즉시 인용 변환과 ` ``` `·`!!!` Enter 코드 블록·콜아웃 단축 생성, 라이브 콜아웃·인용 포커스 기반 오른쪽 설정 패널 연결, 라이브 인용·콜아웃 멀티라인 편집 줄 범위 포커스와 위/아래 방향키 외부 문단 이탈, 인용 Backspace 문단 복귀, 라이브 방향키 이동 시 편집 가능한 줄·카드형 블록 탐색, 라이브 코드·콜아웃·토글 내부 줄 삭제와 마지막 줄 블록 삭제, 라이브 이미지·갤러리 드래그 병합·추가·분리 UI, 갤러리 비율 기반 행 레이아웃, 라이브 이미지·갤러리 hover 삭제 액션, 리스트 마커 파란 계열 통일과 여러 줄 항목 첫 줄 기준 정렬 |
| components/content/ContentMarkdownRenderer.vue | 마크다운 문자열 기반 본문 렌더링, 문단 text-base(16px), 빈 줄 spacer 보존·hard break `<br>` 처리, 표준 마크다운 표 파싱·렌더링(셀 내부 인라인 링크와 `<br>` 줄바꿈 포함), 공개 게시물 인아티클 광고를 본문 길이에 따라 0~2회 삽입, Obsidian식 위첨자·아래첨자 인라인 렌더링, 확장 블록 파싱, `:::` fenced 블록 원본 범위 보정, 닫히지 않은 코드 펜스 하위 콘텐츠 보호, 인용 막대 색상 옵션(`> [!bg=...]`), 라이브 문단 `>` 즉시 인용 변환과 ` ``` `·`!!!` Enter 코드 블록·콜아웃 단축 생성, 라이브 콜아웃·인용 포커스 기반 오른쪽 설정 패널 연결, 라이브 인용·콜아웃 멀티라인 편집 줄 범위 포커스와 위/아래 방향키 외부 문단 이탈, 인용 Backspace 문단 복귀, 라이브 방향키 이동 시 편집 가능한 줄·카드형 블록 탐색, 라이브 코드·콜아웃·토글 내부 줄 삭제와 마지막 줄 블록 삭제, 라이브 이미지·갤러리 드래그 병합·추가·분리 UI, 본문 표시용 WebP 이미지 우선 표시와 원본 fallback, 단일 이미지·갤러리 통합 라이트박스, 갤러리 비율 기반 행 레이아웃, 라이브 이미지·갤러리 hover 삭제 액션, 리스트 마커 파란 계열 통일과 여러 줄 항목 첫 줄 기준 정렬 |
| components/content/ContentMarkdownEditableInline.vue | 라이브 모드 공통 인라인 편집 영역, 일반 인라인 마크다운·Obsidian식 첨자 렌더링과 plain text 멀티라인 본문·끝 줄바꿈 보존, 한글 IME 조합 확정 Enter의 블록별 동작 연결, 위/아래 인접 블록 이동 시 편집 스크롤 추적, Shift 위/아래 인접 블록 선택 확장·단계적 `Cmd/Ctrl+A` 처리, 멀티라인 Enter 텍스트 값 치환, 첫 줄 빈 줄 포함 줄바꿈 유지 |
| components/content/ContentMarkdownInlineSegments.vue | 공개·라이브 본문 공통 인라인 세그먼트 렌더링(링크·굵게·기울임·인라인 코드·첨자·인라인 색상), 링크·색상 내부 중첩 인라인 렌더링, 사용자 화면 fragment 주석 반복을 줄이는 단일 루트 구조 |
| lib/markdown-live-selection.js | 라이브 모드 Selection Bridge, 인접 contenteditable 범위 확장·블록/문서 전체 선택·교차 선택 삭제 마크다운 반영, 콜아웃·인용 전체 선택 삭제 시 빈 본문 줄 보존, Selection focus 기준 연속 확장 |
| components/content/ProseHeading.vue | h1~h6 제목, 기본 mt-12 제거 |
| components/content/ProseImage.vue | 본문 내 이미지, 로드 실패·빈 URL placeholder |
| components/content/ProseImage.vue | 본문 내 이미지, 클릭 확대 요청, 본문 표시용 이미지 실패 시 원본 fallback, lazy 로딩, 로드 실패·빈 URL placeholder |
| components/content/ProseList.vue | 목록 |
| components/content/ProseBlockquote.vue | 인용구, 왼쪽 세로 막대형 기본 스타일과 색상 프리셋, 다크모드 기본 인용 텍스트 가독성 보정 |
| components/content/ProseCodeBlock.vue | 코드 블록 공통 셸(다크 배경, 선택 줄번호 gutter, 공개 복사 버튼) |
@@ -186,7 +187,7 @@
| pages/admin/pages/index.vue | 페이지 목록, 제목 아래 주소·공개 보기 hover 아이콘, 홈 표시·섹션·추천 배지, 조회수·통계 모달, 상태 표시, 화면 기준 행 more vert 메뉴(수정·삭제) |
| pages/admin/pages/new.vue | 전체 화면 페이지 작성, HTML 문서 기본 모드 저장, 홈 Pages 진열장 노출 메타, 저장 토스트 |
| pages/admin/pages/[id].vue | 전체 화면 페이지 수정, HTML 문서/일반 텍스트 모드 저장, 홈 Pages 진열장 노출 메타, 설정 패널 삭제, 저장/삭제 토스트 |
| pages/admin/media/index.vue | 미디어 관리, **미디어 라이브러리/카드 썸네일/프로필 이미지** 탭, 글·멤버 목록과 같은 검색창, 파일 직접 추가, 모든 탭 공통 전체/미사용 배지형 필터, 목록 카드 오른쪽 위 사용 개수 배지, 현재 필터 결과의 삭제 가능 항목 전체 선택·선택 삭제, 이미지·비디오·오디오·파일 종류 필터, 원본 fallback·카드 썸네일 사용 배지(카드 썸네일 탭의 카드 배지는 숨김), 대표 이미지 원본 카드 썸네일 생성·재생성, 비디오 프레임 썸네일, 폴더 트리 more vert(폴더 삭제), 검색·드래그·상세 모달 등 |
| pages/admin/media/index.vue | 미디어 관리, **미디어 라이브러리/카드 썸네일/본문 이미지/프로필 이미지** 탭, 글·멤버 목록과 같은 검색창, 파일 직접 추가, 라이브러리 파일 종류 필터와 미사용 토글, 시스템 탭 전체/미사용 배지형 필터, 목록 카드 오른쪽 위 사용 개수 배지, 현재 필터 결과의 삭제 가능 항목 전체 선택·선택 삭제, 이미지·비디오·오디오·파일 종류 필터, 원본 fallback·카드 썸네일·본문 이미지 사용 배지(파생 탭의 해당 배지는 숨김), 대표 이미지 원본 카드 썸네일 생성·재생성, 본문 이미지 생성·재생성, 비디오 프레임 썸네일, 폴더 트리 more vert(폴더 삭제), 검색·드래그·상세 모달 등 |
| pages/admin/navigation/index.vue | 메뉴 관리: 메인 네비게이션/페이지 섹션/하단 네비게이션/추천 사이트 탭, 각 탭 작업줄 왼쪽 추가·오른쪽 저장 버튼, 메인·하단 메뉴 추가 모달, 페이지 섹션 추가 모달·수정·삭제·섹션 드래그 정렬·섹션별 카드형 페이지 순서 변경 모달(추천 우선 표시·섹션에서 제거 포함), 추천 사이트는 공개와 같은 카드 목록+편집 모달(전체 URL·썸네일·파비콘 갱신), 행 more vert(삭제), 저장 대상별 버튼(`네비게이션 저장`/`페이지 섹션 저장`), `useAdminToast` |
| components/admin/AdminRowMoreMenu.vue | 관리자 행 more vert 메뉴(트리거·화면 기준 팝오버) |
| composables/useAdminRowMenu.js | 관리자 행 메뉴 열림 상태·바깥 클릭 닫기 |
@@ -204,12 +205,12 @@
| 파일 | 화면 |
|------|------|
| pages/index.vue | 홈, `site_settings` 커버가 있을 때만 라이트·다크 이미지 지원 `HomeHero`, Featured/Latest, 사이트 설정 표시개수 기준 Latest·Featured·Pages 데이터 제한, 홈 표시 페이지를 섹션별 도서 진열장형 Pages 선반으로 노출, Pages 추천 카드는 왼쪽 위 SVG 아이콘 배지 표시, Featured와 Pages 선반의 독립 이전·다음 버튼 및 문서 레벨 포인터 추적 기반 드래그 슬라이드·스냅(이동 임계값 미만 클릭은 링크 이동, 포인터 캡처는 드래그 확정 뒤 적용), Pages 선반 ResizeObserver 기반 버튼 상태 갱신, Pages 카드 hover 상단 여백 보정, Latest 피드 Compact 기본값·List·Cards 보기, Compact 모바일 썸네일 80px 정사각형·Compact/List 설명 2줄 제한, Latest 메타는 태그가 있을 때만 링크 배지 표시, 카드 오른쪽 아래 화살표는 게시물 상세 링크, Featured는 추천 글이 있을 때만 표시하며 이미지 없는 추천 글은 제목 placeholder 썸네일과 모바일 터치 가로 스크롤·스냅, 끝에서 화살표 비활성 |
| pages/posts/index.vue | 게시물 전체 목록, 사이트 설정 Posts 표시개수 기준 서버 페이지네이션, 태그는 있을 때만 카드 메타에 링크로 표시 |
| pages/posts/index.vue | 게시물 전체 목록, 사이트 설정 Posts 표시개수 기준 서버 페이지네이션, 공용 번호형 페이지네이션, 태그는 있을 때만 카드 메타에 링크로 표시 |
| pages/posts/[slug].vue | `/post/:slug` 리다이렉트 |
| pages/post/[slug].vue | 블로그 글 상세, 첫 태그가 있을 때만 메타 행에 태그 링크, H1~H3 본문 제목을 오른쪽 TOC 상태로 제공, 공개 회원 로그인 세션의 회원이 글쓴이(`author_id`)이면 공유 버튼 옆 새 탭 편집 링크 표시, 게시물 SEO/OG 메타 출력(요약 없으면 본문 fallback), 공유 모달(X/Bluesky/Facebook/LinkedIn/Email/링크복사), 회원 댓글 섹션 |
| pages/tags/index.vue | 태그 전체 목록, 중앙 히어로와 3열 태그 카드, 좌측 컬러 보더/hover 오버레이 |
| pages/tags/[slug].vue | `/tag/:slug` 리다이렉트 |
| pages/tag/[slug].vue | 태그별 글 목록, 사이트 설정 태그 게시물 표시개수 기준 서버 태그 필터·페이지네이션, 태그 헤더 이미지가 있으면 상단 이미지 헤더 표시, 없으면 기본 태그명·설명 + 공통 섹션 패딩을 쓰는 리스트형 게시물 카드, 카드 태그 배지와 오른쪽 아래 화살표 링크 |
| pages/tag/[slug].vue | 태그별 글 목록, 사이트 설정 태그 게시물 표시개수 기준 서버 태그 필터·공용 번호형 페이지네이션, 태그 헤더 이미지가 있으면 상단 이미지 헤더 표시, 없으면 기본 태그명·설명 + 공통 섹션 패딩을 쓰는 리스트형 게시물 카드, 카드 태그 배지와 오른쪽 아래 화살표 링크 |
| pages/pages/[slug].vue | 고정 페이지 상세, HTML 문서 모드는 직접 진입 시 서버 미들웨어 원문 응답·클라이언트 내부 이동 시 현재 URL reload로 서버 원문 응답 재진입 |
| pages/signup.vue | 회원가입 3단계, `emailOtpConfigured`일 때 이메일 OTP·인증번호 받기, 모바일 OTP 입력창 높이 보정, `POST /api/auth/email-otp/request` |
| pages/signin.vue | 로그인, `/forgot-password` 링크 |

View File

@@ -134,13 +134,13 @@
### 공개 URL 구조
- `/posts` - 게시물 전체 목록. 사이트 설정 POST 설정의 `postListPageSize` 값 기준으로 서버 페이지네이션을 사용하며, 페이지 번호가 바뀌면 화면을 상단으로 이동한다.
- `/posts` - 게시물 전체 목록. 사이트 설정 POST 설정의 `postListPageSize` 값 기준으로 서버 페이지네이션을 사용하며, 페이지 번호가 바뀌면 화면을 상단으로 이동한다. 하단 페이지네이션은 이전·다음 화살표와 페이지 번호 직접 이동을 함께 제공하며, 전체 페이지가 많으면 첫 페이지·끝 페이지·현재 페이지 주변 번호를 유지하고 중간 구간은 `...`으로 접는다.
- `/post/:slug` - 개별 게시물 상세
- `/sitemap.xml` - 공개 sitemap XML. 홈, 게시물 목록, 태그 목록, 공개 발행글, 공개 고정 페이지, 글이 있는 태그 URL을 요청 시점 DB 기준으로 자동 생성한다. 멤버십·비공개·초안·예약 대기 글과 `noindex` 글은 포함하지 않는다.
- `/robots.txt` - 크롤러 안내 파일. 관리자·API·회원 설정·로그인 경로를 제외하고 `Sitemap: /sitemap.xml` 절대 URL을 제공한다.
- `/rss.xml`, `/feed.xml`, `/rss` - 최근 공개 발행글 RSS 2.0 피드. 멤버십·비공개·초안·예약 대기 글은 포함하지 않으며, 최신순 최대 50개를 XML로 응답한다. 게시물에 대표 이미지 또는 OG 이미지가 있으면 절대 URL로 변환해 `media:thumbnail`과 `media:content medium="image"`를 함께 포함한다.
- `/tags` - 태그 전체 목록
- `/tag/:slug` - 태그별 게시물 목록. 사이트 설정 POST 설정의 `tagPostListPageSize` 값 기준으로 서버 태그 필터와 페이지네이션을 사용하며, 페이지 번호가 바뀌면 화면을 상단으로 이동한다.
- `/tag/:slug` - 태그별 게시물 목록. 사이트 설정 POST 설정의 `tagPostListPageSize` 값 기준으로 서버 태그 필터와 페이지네이션을 사용하며, 페이지 번호가 바뀌면 화면을 상단으로 이동한다. 하단 페이지네이션은 게시물 전체 목록과 같은 번호형 빠른 이동 UI를 사용한다.
- 태그명에 `/`를 입력하면 태그명은 그대로 저장하고 슬러그에서는 `/`를 `-`로 변환한다. 예: `Xenoblade3/Hero` 태그명은 `xenoblade3-hero` 슬러그로 저장한다.
- `/tag/:slug` 화면의 헤더와 게시물 목록은 공개 목록 공통 섹션 패딩(`site-section-header`, `site-section-body`)을 사용해 메인 컬럼 좌우 여백을 다른 목록 화면과 맞춘다.
- 태그에 `headerImageUrl`이 있으면 `/tag/:slug` 상단에는 기본 태그명·설명 대신 태그 헤더 이미지를 표시한다. `headerTitle`, `headerText`는 이미지 위 오버레이로 표시하며, 둘 다 비어 있으면 이미지 단독으로 표시한다.
@@ -253,10 +253,10 @@ components/content/
- 캡션(표시용): `![](url "캡션")` — 따옴표 안 문자열만 `ProseImage` figcaption으로 표시
- **파일명을 캡션으로 사용** 토글: URL 파일명을 캡션으로 저장·표시(`![](url "파일명")`). 레거시 `![파일명](url)`도 동일하게 해석
- 와이드/풀: `![alt](url){width=wide|full}` 또는 캡션·width 조합
- 렌더링: `ProseImage.vue` (라운드/보더/패널 배경)
- 렌더링: `ProseImage.vue` (라운드/보더/패널 배경). 클릭 시 라이트박스로 확대하며, 같은 게시물 안의 단일 이미지와 갤러리 이미지를 문서 순서대로 하나의 그룹으로 묶어 Esc 닫기·←/→ 이전·다음 탐색을 지원한다.
- 이미지 갤러리
- `:::gallery` ~ `:::` fenced block 내부에 이미지 마크다운 행을 여러 개 작성
- 렌더링: `ContentMarkdownRenderer.vue` (최대 3개 단위 행 + 라이트박스, Esc 닫기·←/→ 이전·다음)
- 렌더링: `ContentMarkdownRenderer.vue` (최대 3개 단위 행 + 게시물 전체 이미지 라이트박스, Esc 닫기·←/→ 이전·다음)
- 갤러리 행은 1개일 때 전체 폭, 2~3개일 때 행 전체 폭을 나눠 쓰며 이미지 로드 후 자연 비율(가로/세로)에 따라 셀 너비를 조정한다.
- 비디오·오디오·파일 카드
- 비디오: `:::video` ~ `:::` (`url`, `title`, `poster`, `caption` 키값 또는 URL 단독 줄)
@@ -768,7 +768,7 @@ components/content/
- 저장된 태그가 없는 게시물에는 상세 메타 행·홈 Latest·`/posts` 목록 카드에 태그 배지나 더미 라벨을 표시하지 않는다.
- 검색엔진 노출 제외가 켜진 글은 robots 메타를 `noindex, nofollow`로 출력한다.
- 공개 상세 화면의 `og:image`와 Twitter large image 카드는 대표 이미지를 기본값으로 사용한다.
- 이미지 블록은 관리자 업로드 API로 이미지를 업로드하고 `![](url)` 또는 파일명 캡션 토글 시 `![](url "파일명")` 형식으로 저장한다. 단독 이미지 파일 URL(`jpg`, `png`, `webp`, `gif`, `avif`, `svg`) 한 줄은 임베드가 아니라 이미지 블록으로 렌더링한다.
- 이미지 블록은 관리자 업로드 API로 이미지를 업로드하고 `![](url)` 또는 파일명 캡션 토글 시 `![](url "파일명")` 형식으로 저장한다. 단독 이미지 파일 URL(`jpg`, `png`, `webp`, `gif`, `avif`, `svg`) 한 줄은 임베드가 아니라 이미지 블록으로 렌더링한다. 게시물 저장 시 `/uploads/posts/YYYY/MM/` 아래의 본문 `jpg`·`png`·`webp` 이미지는 원본 옆 `thumbs/*-body.webp` 표시용 이미지로 자동 생성하며, 공개 본문 단일 이미지와 갤러리는 이 표시용 이미지를 우선 사용하고 실패하면 원본 URL로 대체한다. 라이트박스는 원본 이미지 URL 목록을 기준으로 연다.
- 이미지/갤러리 삽입 시 캡션은 기본 비우며, 블록 설정 패널에서 **파일명을 캡션으로 사용** 토글로 이미지 아래에 URL 파일명을 표시한다.
- 라이브 모드 이미지 블록은 hover/focus 시 우측 상단에 `삭제` 버튼만 표시한다. 이미지 URL·캡션·파일명 캡션 사용 여부 수정은 해당 이미지 블록에 포커스했을 때 오른쪽 이미지 설정 패널에서 처리한다.
- 이미지 블록 표시 옵션은 `regular`, `wide`, `full` 값을 사용하며 `regular`는 width 옵션을 생략한다.
@@ -912,7 +912,7 @@ components/content/
- 관리자 미디어 업로드 API는 이미지·비디오·오디오·문서 확장자를 허용한다(에디터 슬래시·미디어 모달과 동일 목록).
- 업로드 파일 크기 제한은 종류별 환경 변수를 따른다. 이미지·아바타·로고 등은 `MAX_FILE_SIZE`(기본 10MB), 비디오는 `MAX_VIDEO_FILE_SIZE`(기본 200MB), 오디오는 `MAX_AUDIO_FILE_SIZE`(기본 50MB), 문서·ZIP 등은 `MAX_DOCUMENT_FILE_SIZE`(기본 50MB).
- 로컬 개발 업로드 파일은 `public/uploads/posts/YYYY/MM/` 아래 저장하고 `/uploads/posts/YYYY/MM/filename` URL로 제공한다.
- 관리자 미디어 화면 상단에 **미디어 라이브러리**·**카드 썸네일**·**프로필 이미지** 탭을 두어, 라이브러리 탭에서는 원본 업로드 파일만, 카드 썸네일 탭에서는 `/posts/YYYY/MM/thumbs/*-card.webp` 파일만, 프로필 이미지 탭에서는 `/members/avatars/` 파일만 검색·탐색한다. 모든 탭에서 전체/미사용 배지형 필터를 표시하고, 목록 카드 오른쪽 위에는 실제 사용 개수 숫자 배지를 표시한다. 카드 썸네일 탭에서는 모든 항목이 카드 파생 파일이므로 `카드` 배지는 표시하지 않는다.
- 관리자 미디어 화면 상단에 **미디어 라이브러리**·**카드 썸네일**·**본문 이미지**·**프로필 이미지** 탭을 두어, 라이브러리 탭에서는 원본 업로드 파일만, 카드 썸네일 탭에서는 `/posts/YYYY/MM/thumbs/*-card.webp` 파일만, 본문 이미지 탭에서는 `/posts/YYYY/MM/thumbs/*-body.webp` 파일만, 프로필 이미지 탭에서는 `/members/avatars/` 파일만 검색·탐색한다. 라이브러리 탭의 파일 종류 필터는 `전체/이미지/영상/음악/파일`을 표시하고 사용 상태 필터는 중복 `전체` 배지를 피하기 위해 `미사용` 토글만 표시한다. 시스템 파생 탭에서는 전체/미사용 배지형 필터를 표시하고, 목록 카드 오른쪽 위에는 실제 사용 개수 숫자 배지를 표시한다. 카드 썸네일 탭과 본문 이미지 탭에서는 모든 항목이 해당 파생 파일이므로 각각 `카드`, `본문` 배지는 표시하지 않는다.
- 미디어 라이브러리 탭은 왼쪽 폴더 트리와 오른쪽 고밀도 썸네일 갤러리, 검색, 파일명 변경, 개별 삭제를 제공한다.
- 관리자는 폴더 추가 버튼으로 모달에서 새 폴더·하위 폴더 이름을 입력해 만들 수 있으며, `미분류`·`썸네일`(및 그 하위)을 제외한 폴더는 목록에서 삭제할 수 있다. 폴더 삭제 시 해당 경로 및 하위 경로로 분류돼 있던 미디어 메타는 모두 `미분류`로 되돌린다.
- 썸네일 본문(이미지·파일명) 한 번 클릭 시 상세(미리보기) 모달이 열리고, 썸네일 좌측 상단 **선택 토글**로 개별 선택한다. Shift+클릭으로 범위 선택이 가능하다. 현재 필터 결과의 전체 선택은 사용 중이 아니어서 삭제 가능한 항목만 대상으로 한다.

View File

@@ -8,6 +8,21 @@
## 프론트엔드 개발
- 공개 게시물 본문 이미지 최적화 후속 진행.
- 현재 게시물 저장 시 본문 표시용 WebP(`thumbs/*-body.webp`, 1600px)가 생성되고 공개 본문에서 우선 표시됨.
- 기존 업로드 이미지 전체에 대한 백필 스크립트가 아직 없으므로 과거 게시물은 다시 저장하거나 미디어 상세에서 개별 생성해야 함.
- 화면 크기별 `picture`/`srcset`과 갤러리 미리보기용 더 작은 파생 이미지(예: 720px)는 아직 미구현.
- 라이트박스는 현재 원본 URL을 사용하므로, 초고용량 원본이 많은 경우 큰 파생 이미지와 원본 보기 옵션 분리 검토.
- 업로드 총량 제한 및 메모리 사용량 개선.
- 현재 관리자 업로드는 파일별 제한만 있고 한 번에 올리는 파일 개수·총 용량 제한이 없음.
- 서버는 multipart 전체를 메모리에 받은 뒤 검사하므로, 고용량 다중 업로드 시 메모리 피크가 생길 수 있음.
- 클라이언트·서버 양쪽에 총 파일 수/총 바이트 제한을 추가하고, 장기적으로 스트리밍 업로드 파서 도입 검토.
- 업로드 파일 캐시 정책 개선.
- 현재 `/uploads/**` 응답은 `cache-control: no-cache`라 이미지가 많은 페이지에서 재검증 요청이 늘어날 수 있음.
- 업로드 파일명은 충돌 방지로 고유하게 저장되므로 `public, max-age=31536000, immutable` 적용 가능 여부 검토.
- 공개 통합 검색 인덱스 개선.
- 현재 검색은 게시물·페이지 `content`에 `lower(content)` 부분 일치를 수행함.
- 게시물이 수백~수천 개로 늘면 검색 모달 입력마다 DB 스캔 비용이 커질 수 있으므로 PostgreSQL full-text search, `pg_trgm`, 검색용 정규화 컬럼 중 하나로 전환 검토.
## 콘텐츠 스타일 구현

View File

@@ -1,5 +1,25 @@
# 업데이트 이력
## v1.10.3
- 공개 게시물 목록과 태그 게시물 목록 페이지네이션을 번호형 빠른 이동 UI로 교체.
- 페이지 수가 많을 때 처음·끝 페이지와 현재 페이지 주변 번호를 유지하고 중간 구간을 `...`으로 접도록 정리.
- 좁은 화면에서 페이지 번호가 넘칠 때 가로 스크롤로 탐색할 수 있도록 보정.
## v1.10.2
- 게시물 저장 시 본문 이미지의 WebP 표시용 파생 파일(`thumbs/*-body.webp`) 생성 추가.
- 공개 본문 단일 이미지·갤러리 표시에서 본문 표시용 이미지를 우선 사용하고 실패 시 원본으로 대체하도록 수정.
- 관리자 미디어에 `본문 이미지` 탭 추가 및 원본 상세의 본문 이미지 생성·재생성 액션 추가.
- 관리자 미디어 라이브러리의 중복 `전체` 필터 배지 제거 및 `미사용` 토글 동작 정리.
## v1.10.1
- 공개 본문 단일 이미지를 클릭하면 라이트박스로 크게 볼 수 있도록 수정.
- 게시물 안의 단일 이미지와 갤러리 이미지를 하나의 라이트박스 그룹으로 묶어 이전·다음 탐색이 이어지도록 정리.
- 본문 단일 이미지와 갤러리 이미지에 lazy 로딩·비동기 디코딩 속성 추가.
- 이미지 최적화·업로드 총량 제한·업로드 캐시·검색 인덱스 개선 과제를 `docs/todo.md`에 기록.
## v1.10.0
- 관리자 글쓰기 자동 저장 중 추가 입력한 본문·태그가 저장 완료 처리와 화면 이동으로 사라지지 않도록 수정.