## 2026-07-29 v1.12.6 — 인기 콘텐츠 순위는 유지하고 참여 지표만 보정한다
인기 게시물·페이지 목록은 관리자가 어떤 콘텐츠가 많이 보는지 빠르게 파악하는 영역이므로, 원장 데이터가 일부 기간에만 있을 때 순위 자체를 원장 기준으로 바꾸면 운영자가 보던 목록이 불필요하게 흔들릴 수 있다. 따라서 순위와 조회수·방문자 수는 기존 일별 집계 기준으로 유지하고, 읽음·평균 체류·스크롤처럼 콘텐츠 세션 중복 제거가 중요한 부가 지표만 원장 데이터가 있을 때 보정한다.
## 2026-07-29 v1.12.5 — 관리자 기간 지표는 원장 우선으로 보정한다
콘텐츠 세션 원장은 같은 글이나 페이지를 보는 한 번의 진입을 하나의 상세 기록으로 보존하므로, 평균 체류시간과 스크롤 도달률처럼 중복 이벤트에 민감한 지표를 더 안정적으로 계산할 수 있다. 다만 원장 행은 방문자 해시와 함께 32일만 보관하므로, 전체 누적이나 오래된 기간까지 원장만으로 다시 계산하면 과거 데이터가 빠진 것처럼 보일 수 있다. 따라서 관리자 요약과 게시물·페이지 상세의 최근 기간 지표는 원장 데이터가 보관 범위 안에 있을 때만 우선 사용하고, 그 밖의 범위는 기존 일별 누적 집계를 그대로 사용한다.
## 2026-07-29 v1.12.4 — 통계 집계 기준을 콘텐츠 세션 원장으로 분리한다
기존 통계 수집은 탭 단위 실시간 세션을 현재 접속자 표시와 체류·스크롤 증분 계산에 함께 사용했다. 이 구조에서는 같은 탭에서 여러 게시물이나 페이지를 이동할 때 이전 콘텐츠의 체류시간과 스크롤 비율이 다음 콘텐츠 집계에 영향을 줄 수 있다. 따라서 클라이언트는 탭 세션과 별도로 라우트 진입 단위 `contentSessionId`를 보내고, 서버는 원문 ID 대신 해시된 `analytics_content_sessions` 원장을 기준으로 pageview/read 중복과 heartbeat 증분을 판단한다. `analytics_active_sessions`는 현재 접속자 표시용으로 남겨 실시간 UI 책임과 콘텐츠 분석 책임을 분리한다.
## 2026-07-29 v1.12.2 — Google 계정 재연결은 소유자 관리로 제한한다
회원 본인이 Google 계정을 다시 연결할 수 있으면 실수로 다른 Google 계정에 바꾸거나 관리자 계정의 로그인 경로가 예상과 달라질 수 있다. 따라서 회원 설정 화면은 연결 전에는 연결 버튼만, 연결 후에는 읽기 전용 상태만 표시한다. 예외적으로 계정 이전이나 잘못된 연결을 정리해야 할 수 있으므로, 해제 권한은 관리자 멤버 상세 화면의 소유자 전용 작업으로 둔다.
## 2026-07-29 v1.12.1 — Google 로그인은 기존 계정 연결 방식으로 추가한다
Google 계정은 사이트 계정을 대체하지 않고 외부 식별자(`sub`)로 연결한다. 일반 회원은 Google 로그인 시 연결된 계정이 있으면 그 계정으로 로그인하고, 없으면 이메일 중복이 없을 때 사이트 전용 회원을 새로 만든다. 관리자는 보안상 자동 생성이나 이메일 매칭을 허용하지 않고, 기존 관리자 계정으로 먼저 로그인한 뒤 설정 화면에서 직접 Google 계정을 연결한 경우에만 관리자 Google 로그인을 허용한다.
## 2026-07-29 v1.12.0 — v1.11 내부 개선 구간을 마감한다
v1.11에서는 에디터·미디어·Import/Export·보안 의존성처럼 패치 단위 개선이 길게 이어졌다. 상세 작업 이력을 한 문서에 계속 누적하면 최신 상태 확인보다 과거 항목 검색 비용이 커지므로, `docs/update.md`는 v1.11 이후의 최신 이력 중심으로 유지하고 v1.10 이하 상세 기록은 `docs/update-archive.md`로 분리한다. 방문자에게 공유할 변화는 v1.11 개발 구간을 묶어 `docs/update-log/v1.12.md`로 별도 정리한다.
## 2026-07-29 v1.11.128 — 이미지 처리 의존성은 검증 가능한 업데이트로 올린다
운영 의존성 audit에서 직접 이미지 처리 경로인 `sharp`와 압축·빌드 전이 의존성인 `tar`, `brace-expansion` 경고가 다시 확인됐다. `sharp`는 업로드·썸네일 생성에 직접 영향을 주므로 단순 버전 변경만 하지 않고 게시물 카드, 본문 표시용, 페이지 커버 WebP를 실제 생성하는 스모크 테스트를 함께 추가한다. `tar`와 `brace-expansion`은 상위 패키지의 다음 릴리스를 기다리는 동안 npm `overrides`로 패치 버전을 고정해 운영 설치 결과가 취약 버전으로 되돌아가지 않게 한다.
## 2026-07-29 v1.11.127 — 미디어 카탈로그 전환 관찰 모드는 정리한다
미디어 카탈로그는 개발과 운영 전환 과정에서 reconcile과 `shadow` 비교를 거쳐 `database` 모드로 고정하는 흐름을 사용했다. 이제 런타임 전환 UI가 제거되고 운영 계약도 환경 변수 기반으로 굳었으므로, 계속 사용하지 않는 `shadow` 비교 분기는 관리자 미디어 조회의 부수 로그와 비동기 DB 조회만 남긴다. 따라서 허용 모드는 `filesystem`과 `database`로 축소하고, `database` 준비 실패 시 파일시스템 목록으로 되돌아가는 안전 fallback만 유지한다.
## 2026-07-29 v1.11.126 — 미디어 삽입 실행은 에디터 밖에서 연결한다
관리자 Markdown 에디터의 미디어 선택 상태와 업로드 요청은 이미 `useAdminMarkdownMediaPicker`로 분리되어 있었지만, 선택된 항목을 이미지·갤러리·활성 갤러리·슬래시 대기 줄에 실제 적용하는 연결부는 에디터 본체에 남아 있었다. 이 실행부가 본체에 남으면 미디어 모달 상태와 본문 삽입 규칙, 업로드 후 목록 갱신이 한 파일에서 다시 얽힌다. 따라서 `useAdminMarkdownMediaActions`가 선택·업로드 미디어 삽입 계획을 실제 본문 반영으로 연결하고, 에디터는 블록 삽입·활성 갤러리 추가·토스트 같은 외부 콜백만 제공한다.
## 2026-07-29 v1.11.125 — 소스 선택 편집 실행은 composable로 둔다
관리자 Markdown 에디터의 소스 선택 편집 계산은 이미 `markdown-source-selection` helper로 분리되어 있었지만, 계산 결과를 본문에 반영하고 textarea 선택 범위를 복원하는 실행 함수들이 에디터 본체에 남아 있었다. 이 함수들은 단축키, 붙여넣기 HTML 변환, 미디어 삽입 같은 여러 경로에서 함께 쓰이므로 본체에 두면 미디어 연결부를 정리할 때 다시 선택 편집 책임이 섞인다. 따라서 `useAdminMarkdownSourceActions`가 선택 교체, 블록 삽입, 인라인 링크·감싸기, 줄 삭제 실행을 맡고 에디터는 선택 상태와 선택 적용 콜백만 제공한다.
## 2026-07-29 v1.11.124 — 공통 포커스 실행은 에디터 본문 밖으로 둔다
관리자 Markdown 에디터에는 작성 모드 textarea와 라이브 미리보기 편집 줄 사이에서 첫 포커스를 잡고, 히스토리 복원 후 선택 위치나 줄 오프셋을 되돌리는 실행 코드가 남아 있었다. 이 코드는 본문 편집 규칙보다 모드별 포커스 적용에 가까우므로 에디터 본체에 두면 선택 편집·미디어 삽입 로직과 다시 섞인다. 따라서 `useAdminMarkdownEditorFocusActions`가 첫 줄 보정, 첫 블록 포커스, 히스토리 복원 포커스를 맡고 에디터는 DOM 실행 콜백만 제공한다.
## 2026-07-29 v1.11.123 — 슬래시 명령 실행은 전용 composable로 연결한다
슬래시 명령의 후보 계산과 부수 효과 계획은 이미 helper로 분리되어 있었지만, 실제 적용 단계인 원문 줄 제거, 미디어 모달 열기, textarea 선택 복원, 라이브 미리보기 포커스 예약은 에디터 컴포넌트에 남아 있었다. 이 실행 연결부가 컴포넌트에 남으면 슬래시 메뉴 상태와 DOM 포커스 복원이 계속 한 파일에서 커진다. 따라서 `useAdminMarkdownSlashCommandActions`가 명령 적용 실행을 맡고, 에디터는 줄 교체·모달·포커스 콜백만 주입한다.
## 2026-07-29 v1.11.122 — 슬래시 메뉴 위치 관리는 상태와 분리한다
관리자 Markdown 에디터의 슬래시 메뉴는 상태·명령 실행과 별개로 미리보기 줄 DOM, textarea caret 좌표, 뷰포트 보정, 스크롤/resize 리스너, 선택 항목 스크롤 후처리를 함께 관리한다. 이 DOM 위치 관리가 컴포넌트에 남으면 슬래시 명령 정책을 볼 때 화면 배치 코드가 먼저 섞인다. 따라서 `useAdminMarkdownSlashMenuPosition`이 메뉴 좌표 갱신과 리스너 수명, 하이라이트 스크롤을 담당하고 에디터는 메뉴 상태와 명령 실행 연결만 유지한다.
## 2026-07-29 v1.11.121 — 라이브 미리보기 이벤트 적용 실행을 composable로 분리한다
관리자 Markdown 에디터의 라이브 미리보기 이벤트 계산은 helper로 분리되어 있었지만, 갤러리 순서 변경·이미지 병합·문단 삽입·줄 삭제 같은 이벤트를 실제 본문 ref에 반영하고 포커스를 예약하는 연결 함수는 여전히 에디터에 남아 있었다. 이 실행부가 컴포넌트에 남으면 라이브 편집 표면의 DOM 이벤트와 Markdown 값 변경 흐름이 다시 섞인다. 따라서 `useAdminMarkdownPreviewActions`가 미리보기 이벤트 적용을 맡고, 에디터는 줄 교체·fenced 끝줄 보정·패널 초기화·포커스 실행 콜백만 제공한다.
## 2026-07-29 v1.11.120 — 블록 패널 옵션 적용 실행을 composable로 분리한다
관리자 Markdown 에디터의 오른쪽 블록 패널은 이미 옵션 문자열 계산을 helper로 분리했지만, 계산 결과를 현재 문서에 반영하고 패널 포커스 유지·textarea 선택 복원·라이브 미리보기 포커스 예약을 실행하는 연결 함수가 여전히 컴포넌트에 모여 있었다. 이 실행 레이어를 컴포넌트 안에 두면 패널 UI 이벤트가 늘어날수록 에디터 본체가 다시 커진다. 따라서 `useAdminMarkdownBlockActions`가 활성 블록 ref와 실행 콜백을 주입받아 이미지·갤러리·임베드·콜아웃·코드·토글·인라인 색상 적용을 담당하고, 에디터는 DOM 참조와 본문 삽입 함수만 제공한다.
## 2026-07-29 v1.11.119 — 라이브 미리보기 블록 이벤트 계산을 helper로 분리한다
관리자 Markdown 에디터의 라이브 미리보기 이벤트는 갤러리 순서 변경, 단일 이미지 병합, 갤러리 내부 삽입, 이미지 분리, 문단 추가·삭제·병합처럼 Markdown 줄 배열을 직접 재구성한다. 이 계산이 컴포넌트에 남으면 실제 DOM 포커스 실행과 값 변경 규칙이 계속 섞인다. 따라서 `markdown-editor-preview-blocks` helper가 이벤트 payload와 현재 Markdown 값으로 다음 Markdown 값을 계산하고, 에디터는 계산 결과를 반영하는 연결점만 맡는다.
## 2026-07-28 v1.11.118 — 라이브 미리보기 DOM 실행부는 전용 composable이 맡는다
관리자 Markdown 에디터의 라이브 미리보기 포커스 실행은 미리보기 표면 컴포넌트 ref, 실제 DOM 줄 조회, 현재 포커스 줄 계산, 화면 상단 기준 줄 탐색, 전체 선택·스크롤 실행이 함께 필요하다. 이 DOM 실행 세부사항이 에디터 본체에 남으면 모드 전환 브리지와 블록 패널 정책을 볼 때 실제 정책보다 DOM 접근 코드가 먼저 보인다. 따라서 `useAdminMarkdownPreviewFocus`가 미리보기 표면 접근과 현재 source line 계산을 맡고, 에디터는 composable이 제공하는 실행 함수만 연결한다.
## 2026-07-28 v1.11.116 — 블록 옵션 문자열 계산은 에디터 밖에서 만든다
관리자 Markdown 에디터의 오른쪽 패널은 이미지 캡션·갤러리 순서·임베드 URL·콜아웃/코드/토글 선언 줄·인라인 색상처럼 서로 다른 마크다운 문자열을 직접 조립하고 있었다. 이 계산이 컴포넌트 안에 남으면 패널 포커스 유지나 DOM 포커스 복원과 실제 저장 문법이 한 파일에서 계속 얽힌다. 따라서 `markdown-editor-block-options` helper가 줄 교체 계획과 새 본문 값을 만들고, 에디터는 계산 결과를 현재 문서에 적용하는 역할만 맡게 한다.
## 2026-07-28 v1.11.115 — 블록 패널 표시 상태는 편집 본문과 분리한다
관리자 Markdown 에디터의 오른쪽 블록 패널은 textarea 포커스, 라이브 편집 줄 포커스, 패널 자체 포커스, IME 조합 중 직전 컨텍스트 유지, 인라인 색상 선택 우선순위가 함께 결정한다. 이 상태 계산을 본문 변경 함수와 같은 파일에 두면 이미지·콜아웃·코드 옵션을 수정할 때 패널 표시 조건까지 같이 흔들릴 수 있다. 따라서 `useAdminMarkdownBlockPanel`이 표시 상태와 포커스 유지 규칙을 맡고, 에디터는 실제 본문 값을 바꾸는 함수와 선택 컨텍스트 계산만 유지한다.
## 2026-07-28 v1.11.114 — textarea DOM 실행부는 전용 composable이 맡는다
관리자 Markdown 에디터의 textarea 높이 조절, 거터 줄 높이 측정, 현재 커서 줄 계산, 선택 기억과 복원, 지정 줄 스크롤은 모두 DOM 실행 세부사항이다. 이 로직이 모드 전환·블록 패널·슬래시 메뉴 정책과 같은 컴포넌트 안에 섞여 있으면 실제 정책 변경 없이도 에디터 파일이 계속 커진다. 따라서 `useAdminMarkdownTextareaFocus`가 textarea DOM 실행부를 맡고, 컴포넌트는 필요한 상태와 콜백만 주입해 편집 정책과 DOM 적용 단계를 분리한다.
## 2026-07-28 v1.11.113 — 모드 전환 포커스 상태는 별도 브리지가 맡는다
관리자 Markdown 에디터의 작성/라이브 전환은 textarea 선택 위치, 라이브 편집 줄, 코드·콜아웃 같은 실제 편집 가능 줄 보정, 전환 후 DOM 포커스 실행 순서가 함께 얽힌다. 실제 DOM 조작은 부모 컴포넌트의 참조가 필요하지만, 어느 위치를 대기 상태로 저장하고 모드 변경 때 어떤 복원 콜백을 호출할지는 독립된 상태 흐름이다. 따라서 `useAdminMarkdownFocusBridge`가 pending focus와 모드 watch를 맡고, 에디터는 DOM 콜백만 주입해 전환 복원 정책을 테스트 가능한 단위로 둔다.
## 2026-07-28 v1.11.112 — 슬래시 명령 부수 효과 판단을 에디터 밖으로 둔다
슬래시 명령 적용 후 미디어 모달을 열지, 마크다운 줄을 삽입한 뒤 textarea를 볼지 라이브 미리보기를 볼지는 DOM 조작보다 명령 정책에 가깝다. 이 판단이 에디터 컴포넌트 안에 남아 있으면 같은 명령 흐름을 고칠 때 상태 정리와 포커스 실행 조건이 함께 흔들린다. 따라서 `markdown-slash-commands`가 적용 계획을 에디터 효과 계획으로 한 번 더 정규화하고, 컴포넌트는 계산된 계획에 따라 실제 본문 교체와 포커스 복원만 수행하게 한다.
## 2026-07-28 v1.11.111 — 미디어 요청 실행은 미디어 선택 상태와 함께 둔다
관리자 Markdown 에디터의 미디어 모달은 이미 검색어, 페이지, 선택 상태를 `useAdminMarkdownMediaPicker`에서 관리하지만, 실제 서버 목록 조회와 업로드 실행은 부모 에디터에 남아 있었다. 이 상태에서는 모달 상태가 바뀌는 이유와 API 요청 결과가 반영되는 이유가 서로 다른 파일에 흩어진다. 미디어 선택 composable이 목록 조회, 업로드 한도 검증, 업로드 후 라이브러리 탭 전환과 선택 상태 갱신까지 맡게 하고, 에디터는 업로드·선택 결과를 본문 마크다운에 반영하는 일만 유지한다.
## 2026-07-28 v1.11.110 — 슬래시 메뉴는 실행할 후보가 있을 때만 입력을 잡는다
슬래시 메뉴가 열려 있다는 이유만으로 Enter를 먼저 막으면 `/ㄴㅇ`처럼 명령 후보가 없는 일반 텍스트도 줄바꿈할 수 없다. 슬래시 입력은 명령 실행 단축키이기 전에 문서 안에 남을 수 있는 문자이므로, 메뉴가 키보드 이벤트를 가로채는 기준을 “표시할 후보가 있는가”로 좁힌다. 후보가 없으면 메뉴 상태만 정리하고 라이브·소스 편집기의 일반 줄바꿈과 커서 이동 흐름을 그대로 따른다.
## 2026-07-27 v1.11.109 — 슬래시 메뉴 취소는 같은 입력에만 유지한다
슬래시 메뉴를 ESC로 닫은 뒤 같은 줄에서 다시 열리지 않도록 억제하는 처리는 필요하지만, 줄 번호만 기준으로 억제하면 사용자가 `/`를 지웠다가 다시 입력해도 새 입력으로 인식하지 못한다. 취소는 “현재 입력을 잠시 무시”하는 의미이지 “그 줄의 슬래시 메뉴를 계속 금지”하는 의미가 아니므로, 억제 상태에 취소 당시 줄 내용을 함께 저장하고 줄 내용이 바뀌면 억제를 해제한다.
## 2026-07-27 v1.11.108 — 미디어 업로드 규칙을 에디터 밖에서 검증한다
관리자 Markdown 에디터의 미디어 업로드는 서버 요청 자체보다 파일 목록 정규화, multipart 본문 생성, API 응답 파일 추출, 붙여넣기·드롭 시 이미지 필터링, 단일/갤러리 삽입 대상 선택 같은 작은 규칙이 많다. 이 규칙을 컴포넌트 안에 두면 요청 실행과 사용자 입력 판별이 섞여 회귀 테스트를 쓰기 어렵다. 따라서 업로드 전후 계산은 `markdown-editor-media` helper로 옮기고, 에디터는 `$fetch` 실행과 성공 후 본문 반영·토스트 표시만 맡도록 책임을 더 좁힌다.
## 2026-07-27 v1.11.107 — 미디어 선택 모달 상태를 에디터 밖으로 옮긴다
관리자 Markdown 에디터의 미디어 선택 모달은 검색어, IME 입력, 탭, 페이지네이션, 선택 순서, 업로드 중 닫기 방지, 업로드 직후 선택 상태까지 함께 관리한다. 이 상태를 에디터 본문 삽입 함수와 같은 파일에 두면 모달 UI 상태 변경과 실제 마크다운 편집 부수 효과가 계속 섞인다. 따라서 모달 상태와 표시 계산은 `useAdminMarkdownMediaPicker`가 담당하고, 선택한 미디어를 어떤 방식으로 삽입할지 계산하는 규칙은 `markdown-editor-media` helper로 분리한다. 에디터는 서버 조회·업로드·본문 삽입 실행만 맡도록 책임을 좁힌다.
## 2026-07-27 v1.11.106 — 슬래시 명령 적용 판단을 helper로 모은다
관리자 Markdown 에디터의 슬래시 명령은 미디어 선택 모달을 여는 명령과 마크다운 줄을 바로 삽입하는 명령으로 나뉜다. 이 판단을 컴포넌트 안에 두면 메뉴 상태, 줄 교체, 포커스 복원, 모달 열기가 한 함수에서 섞이므로 `markdown-slash-commands` helper가 적용 계획을 계산하게 한다. 컴포넌트는 계산된 계획에 따라 실제 UI 부수 효과만 수행해 이후 미디어 모달과 포커스 연결을 더 작게 분리할 수 있게 한다.
## 2026-07-27 v1.11.105 — 라이브 전환 포커스 계산을 분리한다
관리자 Markdown 에디터가 소스 모드에서 라이브 모드로 넘어갈 때 코드 펜스 선언 줄, 콜아웃·토글 선언 줄, 인용 옵션 줄처럼 실제 contenteditable 줄이 따로 있는 위치를 보정해야 한다. 이 계산을 컴포넌트 안에 두면 DOM 포커스 적용 코드와 마크다운 구조 판별이 섞이므로 `markdown-editor-focus` helper로 분리한다. 컴포넌트는 현재 선택 위치를 읽고 실제 포커스를 적용하는 일만 맡고, 코드 펜스 범위와 라이브 편집 대상 줄 계산은 단위 테스트로 고정한다.
## 2026-07-27 v1.11.104 — 소스 선택 편집 계산을 분리한다
관리자 Markdown 에디터의 소스 모드 선택 편집은 textarea DOM 포커스와 본문 문자열 치환 계산이 한 함수 안에 섞여 있었다. 선택 범위로 블록 조각을 넣거나 인라인 링크·굵게·줄 변환을 적용하는 규칙은 DOM 없이도 검증할 수 있으므로 `markdown-source-selection` helper로 분리한다. 컴포넌트는 textarea 포커스와 스크롤 복원만 담당하고, 문자열 변경 결과와 다음 선택 위치는 순수 함수에서 계산해 이후 미디어 모달·슬래시 명령 분리 시 같은 선택 규칙을 재사용할 수 있게 한다.
## 2026-07-27 v1.11.103 — 관리자 Markdown 히스토리 상태를 분리한다
관리자 Markdown 에디터의 undo/redo는 본문 값, 마지막 선택 위치, 빠른 입력 그룹화, 복원 중 기록 방지를 함께 관리한다. 이 상태를 거대 에디터 컴포넌트 안에 두면 DOM 포커스 복원 코드와 히스토리 스택 규칙이 섞이므로 `useAdminMarkdownHistory`로 분리한다. textarea와 라이브 편집 줄에 실제 포커스를 되돌리는 책임은 DOM 참조가 있는 에디터에 남겨 composable은 순수 상태 전환과 복원 콜백 호출만 담당한다.
## 2026-07-27 v1.11.102 — 공개 Markdown 인용 편집 이벤트를 분리한다
인용 블록 편집은 옵션 선언 줄 보존, 브라우저 자동 보정으로 생기는 `>. ` 접두 정리, 마지막 줄 아래 문단 이탈, Backspace 기반 일반 문단 복귀를 함께 처리한다. 이 흐름을 렌더러 내부에 남기면 공개 본문 표시와 관리자 라이브 편집 규칙이 계속 섞이므로 `useContentMarkdownQuotes`로 분리한다. 이전 줄 단위 인용 편집 함수는 현재 템플릿에서 사용하지 않으므로 제거해 실제 동작 경로만 테스트 대상으로 유지한다.
## 2026-07-27 v1.11.101 — 공개 Markdown 목록 편집 이벤트를 분리한다
목록 편집은 항목 내용 저장뿐 아니라 원본 번호 유지, raw 마커 보존, Enter로 항목 분리·목록 탈출·아래 문단 삽입을 함께 다룬다. 이를 `ContentMarkdownRenderer` 안에 계속 두면 템플릿과 목록 문법 직렬화가 섞이므로 `useContentMarkdownLists`로 분리한다. 빈 목록 마커는 `- `처럼 본문이 없는 줄도 정확히 목록으로 판별해야 하므로 공통 목록 마커 유틸을 보정해 라이브 편집의 목록 탈출 동작을 테스트 가능한 단위로 유지한다.
## 2026-07-27 v1.11.100 — 공개 Markdown 문단 편집 이벤트를 분리한다
문단 편집은 일반 텍스트 커밋뿐 아니라 빈 줄 스페이서 입력, `>` 인용 변환, 코드·콜아웃 단축 블록 생성까지 담당한다. 이 흐름을 렌더러 안에 남기면 템플릿과 라이브 입력 규칙이 계속 섞이므로 `useContentMarkdownParagraphs`로 분리한다. 렌더러는 문단 편집 컴포넌트 이벤트 연결에 집중하고, 문단 단축 문법과 포커스 예약은 composable에서 테스트 가능한 단위로 유지한다.
## 2026-07-27 v1.11.99 — 공개 Markdown fenced 블록 편집 이벤트를 분리한다
코드·콜아웃·토글은 모두 여는 줄과 닫는 줄을 보존하면서 본문을 갱신하고 아래 문단으로 이탈하는 흐름을 공유한다. 이를 `ContentMarkdownRenderer` 안에 각각의 이벤트 함수로 남겨두면 라이브 편집 템플릿과 마크다운 줄 직렬화 책임이 계속 섞인다. `useContentMarkdownFencedBlocks`로 분리해 렌더러는 블록 컴포넌트 이벤트를 연결하고, fenced 블록 문법 보존과 다음 문단 포커스 준비는 composable에서 담당하게 한다.
## 2026-07-27 v1.11.98 — 공개 Markdown 라이브 선택과 포커스 상태를 분리한다
라이브 편집 모드의 포커스 복원, Shift 범위 선택, 전체 선택, 선택 삭제, line focus/blur 이벤트는 본문 블록 렌더링보다 편집 세션 상태에 가깝다. 이를 `ContentMarkdownRenderer` 안에 두면 블록 렌더링 이벤트와 DOM 선택 중개가 섞여 파일을 줄여도 todo의 실제 범위가 보이지 않는다. `useContentMarkdownLiveSelection`으로 옮겨 렌더러는 블록별 편집 이벤트 연결에 집중하고, 남은 todo는 실제 블록 편집 도메인 분리로 좁힌다.
## 2026-07-27 v1.11.97 — 공개 Markdown 이미지 드래그 상태를 렌더러 밖으로 옮긴다
단일 이미지와 갤러리의 라이브 드래그는 드래그 payload, drop target 표시, 병합·삽입·분리·정렬 이벤트를 함께 다루지만, 실제 본문 렌더링 UI와는 별도 상태다. `ContentMarkdownRenderer` 안에 계속 두면 이미지 UI를 수정할 때 렌더링·편집·드래그 책임이 한 파일에서 얽히므로, `useContentMarkdownImageDrag`로 분리해 렌더러는 템플릿 연결과 emit 위임만 담당하게 한다.
## 2026-07-27 v1.11.96 — 공개 Markdown 블록 계산을 렌더러 밖으로 옮긴다
`ContentMarkdownRenderer`는 공개 본문 표시와 관리자 라이브 편집을 모두 연결하므로, 파싱·광고 삽입·제목 ID·라이트박스 대상 같은 계산까지 같은 파일에 있으면 실제 UI 책임과 데이터 준비 책임이 섞인다. 본문 문자열에서 렌더링 블록을 준비하는 계층을 `useContentMarkdownBlocks`로 분리해 렌더러는 준비된 블록을 화면에 배치하고 라이브 편집 이벤트를 중개하는 역할에 더 집중하도록 한다.
## 2026-07-27 v1.11.95 — 공개 Markdown 라이트박스 상태를 분리한다
공개 Markdown 렌더러는 본문 블록 표시와 관리자 라이브 편집 중개가 함께 있어 작은 전역 이벤트 상태가 파일 안에 남아도 추적 비용이 커진다. 단일 이미지·갤러리 라이트박스의 열린 이미지 목록, 현재 인덱스, 좌우 이동, ESC 닫기, 키보드 이벤트 연결/해제를 `useContentMarkdownLightbox`로 분리해 렌더러는 클릭한 이미지 키를 전달하고 UI를 표시하는 역할에 집중한다.
## 2026-07-27 v1.11.94 — Markdown 에디터의 입력 상태와 단축키 판별을 분리한다
관리자 Markdown 에디터는 미디어 선택, 슬래시 명령, 히스토리, 블록 패널이 한 파일에서 맞물려 동작하므로, 작은 조건문을 계속 파일 안에 남기면 회귀 원인을 찾기 어려워진다. 전역 단축키 판별은 순수 helper로, 슬래시 메뉴 refs·computed·억제 줄·키보드 탐색 타이머는 composable로 분리해 에디터 컴포넌트가 실제 편집 흐름 연결에 더 집중하도록 정리한다.
## 2026-07-23 v1.11.18 — 게시물 Import는 작업 단위로 상태를 남긴다
대량 ZIP Import를 단일 HTTP 요청 안에서 끝내면 브라우저 종료, 네트워크 단절, 서버 재시작 시 사용자가 어느 게시물까지 처리됐는지 확인하기 어렵다. 게시물 생성은 자산 저장 이후에만 수행하되, 참조 자산이 누락된 게시물은 불완전한 글을 만들지 않고 게시물별 오류로 남긴다. Import도 Export처럼 PostgreSQL job/item 테이블과 advisory lock 기반 워커를 사용해 진행 상태를 남기고, 같은 slug는 재시도 시에도 스킵해 중복 게시물 생성을 막는다.
## 2026-07-23 v1.11.3 — 구형 블록 에디터 파일을 제거한다
Markdown-first 에디터가 현재 게시물 작성 흐름을 담당하고 있고, `AdminBlockEditor.vue`는 실제 코드에서 더 이상 참조되지 않는다. 이전 동작을 되돌리기 위한 맥락은 Git 이력과 문서에 남아 있으므로, 실행 경로에 연결되지 않은 큰 컴포넌트 파일을 계속 보관하기보다 제거해 관리자 에디터 구조를 단순하게 유지한다.
## 2026-07-22 v1.11.0 — v1.10 구간을 minor로 마감하고 패치 기준을 다시 잡는다
v1.10.0부터 v1.10.88까지 이어진 보안·백업·미디어 카탈로그·검증 보강을 공개 기록 `v1.11`로 묶고 관리 버전을 `1.11.0`으로 올린다. 이후 일상 수정은 `1.11.1`부터 패치로 쌓아, minor 단위 공지와 개발 버전 번호를 다시 맞춘다.
## 2026-07-22 v1.10.88 — 개발 미디어 카탈로그를 database로 고정하고 임시 전환 UI를 제거한다
개발에서 reconcile·shadow·database 비교가 끝난 뒤에는 런타임 버튼으로 모드를 바꾸지 않는다. 임시 전환 패널과 `catalog-mode`·`catalog-status` API, 프로세스 메모리 덮어쓰기를 제거하고 `MEDIA_CATALOG_MODE` 환경 변수만 사용한다. 개발 env는 `database`로 고정한다. 운영도 같은 계약으로 가되, 배포 시 마이그레이션과 안전 reconcile을 먼저 끝내고 나서 `database`로 고정한다. DB 조회가 준비되지 않으면 파일시스템 목록으로 대체하는 fallback은 유지한다.
## 2026-07-21 v1.10.88 — 검증·브라우저 정책·미디어 인덱스는 운영 데이터와 단계적으로 분리한다
API와 브라우저 회귀 테스트가 개발 DB와 실제 업로드 디렉터리를 공유하면 테스트 게시물·파일이 남거나 운영에 가까운 데이터를 훼손할 수 있다. Playwright는 실행마다 이름이 `_test`로 끝나는 별도 PostgreSQL 컨테이너와 임시 업로드 루트만 사용하고 종료 시 볼륨과 파일을 제거한다. 테스트 결과를 비결정적으로 만드는 Export worker와 추천 사이트 파비콘 cron은 테스트 환경에서 끄고, DB 상태를 순서대로 검증하는 현재 시나리오는 worker 1개로 직렬 실행한다.
CSP는 안전한 지시어를 즉시 강제하되 기존 사용자 사이트 코드, 광고, 외부 미디어를 한 번에 차단하지 않는다. `base-uri`, `object-src`, `frame-ancestors`만 최소 강제 정책으로 분리하고 실행·스타일·이미지·프레임 출처는 Report-Only에서 관찰한다. 완전한 HTML 문서를 의도적으로 제공하는 `/pages/**`는 기존 콘텐츠의 ``와 독립 자산 구성을 보존하기 위해 강제 정책에서 제외하고 보고만 수집한다. 보고 API는 인증 없는 브라우저 입력이므로 크기·형식·IP별 빈도를 제한하고, URL 원문 대신 문서 경로·차단 origin·지시어·브라우저 계열만 집계해 호환성 판단에 필요한 정보만 남긴다.
미디어 파일의 실체와 최종 제공 책임은 계속 파일시스템에 두고, PostgreSQL 카탈로그는 검색·필터·사용처·페이지네이션을 빠르게 제공하는 인덱스로 둔다. `filesystem`을 기본값으로 유지한 뒤 안전 reconcile로 파일을 변경하지 않고 카탈로그와 참조를 채우며, `shadow`에서 두 결과의 개수·용량 차이를 관찰한 후에만 `database`로 전환한다. DB 모드도 reconcile 미실행, 미완료 행, 개수·용량 불일치 또는 조회 오류가 있으면 파일시스템으로 되돌아가므로 카탈로그 장애가 미디어 관리 중단으로 이어지지 않는다. 앱이 관리하는 업로드·생성·변경·삭제와 콘텐츠 참조에는 최선 노력 dual-write를 적용하고, 외부에서 사라진 파일은 reconcile 시 삭제하지 않고 `missing`으로 표시해 운영자가 상태를 확인할 수 있게 한다.
## 2026-07-21 v1.10.87 — 독립 편집 영역은 저장 성공 범위를 서로 침범하지 않는다
사이트 설정은 여러 카드를 동시에 편집할 수 있지만 카드 하나를 저장할 때 폼 전체를 PUT하면, 다른 카드에 아직 저장하지 않은 입력이나 다른 요청이 먼저 반영한 값을 오래된 화면 값으로 되돌릴 수 있다. 각 카드는 자신이 담당하는 필드만 PATCH하고 서버도 전달된 DB 컬럼만 원자적으로 갱신하도록 변경한다. 성공 응답도 해당 카드 필드에만 반영해 화면 안의 다른 미저장 입력을 보존한다.
게시물 초안 자동 저장은 편의를 위한 기능이므로 실패했는데도 이탈을 허용하면 안 된다. 이탈 직전 저장이 실패하거나 이미 저장 중이거나 유효한 payload를 만들 수 없으면 현재 화면에 남기고, 페이지 편집도 같은 미저장 이탈 확인을 사용한다. 저장 요청 중 사용자가 추가로 입력한 페이지 값은 요청에 실제로 보낸 payload와 비교해 남은 변경으로 유지한다.
게시물 HTML 블록은 관리자 입력이라도 Import 파일이나 탈취된 관리자 세션을 통해 실행성 코드가 저장될 수 있다. 정규식으로 위험 문자열 몇 개를 지우는 방식은 깨진 태그, 문자 참조로 우회한 URL, SVG 계열 속성을 HTML parser처럼 해석할 수 없다. 따라서 표·문단·목록·기본 이미지처럼 본문에 필요한 태그와 속성만 허용하는 `sanitize-html` parser를 사용하고, 완전한 HTML 문서를 의도적으로 제공하는 고정 페이지 렌더링과는 정책을 분리한다.
일반 미디어 업로드도 Import와 같은 원칙으로 요청 본문 전체를 메모리에 올리지 않는다. 파일은 업로드 디렉터리 안의 요청별 임시 폴더로 스트리밍하고 파일 수·전체 용량·종류별 용량을 읽는 동안 제한한 뒤, 검증된 파일만 hard link의 배타 생성으로 최종 이름에 옮긴다. 이렇게 하면 같은 이름의 동시 업로드가 기존 파일을 덮어쓰지 않고 실패한 요청의 부분 파일도 한곳에서 정리할 수 있다.
미디어 목록은 디스크 파일과 모든 콘텐츠 사용처를 결합하므로 페이지 번호만 바뀌어도 동일한 전체 스캔을 반복할 이유가 없다. 한 프로세스 안에서 30초 결과 캐시와 진행 중 Promise를 공유하고 파일 변경 API가 캐시를 즉시 무효화한다. Export 작업은 프로세스 메모리 Set만으로 중복을 막지 않고 작업 ID별 PostgreSQL advisory lock을 실행 연결이 끝날 때까지 유지한다. 서버 시작과 30초 주기 worker가 `queued`·중단된 `processing` 작업을 다시 발견하므로 재시작 복구와 다중 인스턴스 단일 실행을 함께 보장한다.
공개 URL의 존재 여부와 화면 상태는 같은 콘텐츠 정책을 따라야 한다. 등록되지 않은 태그는 게시물이 0개인 유효 태그와 다르므로 404로 응답하고, 멤버십 본문을 볼 수 있는 회원은 그 글의 댓글도 같은 권한으로 조회·작성·좋아요할 수 있게 한다. 라우터가 같은 댓글 컴포넌트를 재사용할 때는 slug 변경을 요청 경계로 삼아 이전 입력과 지연 응답을 폐기한다.
어나운스 바 높이는 문구 줄바꿈과 화면 폭에 따라 달라질 수 있으므로 57px 고정값으로 사이드바 위치를 잡을 수 없다. `SiteTopChrome`이 ResizeObserver로 계산한 `--site-top-chrome-height`를 모바일 왼쪽 메뉴와 데스크톱 양쪽 사이드바의 top·뷰포트 높이에 공통 적용한다.
PostgreSQL은 앱 컨테이너가 Docker 내부 네트워크로 접근하므로 호스트 전체 인터페이스에 포트를 공개할 필요가 없다. 로컬 관리 도구 접근은 유지하되 기본 바인딩을 `127.0.0.1`로 제한한다. 마이그레이션은 SQL 파일만 적용되고 이력 INSERT가 실패하거나 그 반대가 되지 않도록 두 작업을 같은 psql 단일 트랜잭션으로 실행한다.
빌드 성공은 실행 코드의 문법과 번들 가능성만 확인할 뿐 보안 정책의 입력·출력은 검증하지 않는다. Node 내장 테스트 러너를 별도 `npm test`로 두고 HTML sanitizer 우회 사례와 multipart 스트리밍 제한부터 회귀 테스트를 시작한다. 브라우저 방어는 기존 사용자 코드·광고 호환성을 깨지 않도록 CSP를 Report-Only로 먼저 관찰하고, `nosniff`, referrer, frame, 권한 정책은 즉시 적용한다. 공개 통계 수집은 정상 브라우저 빈도보다 충분히 높은 IP별 분당 상한을 두어 단일 출처의 DB 쓰기 증폭을 줄인다.
## 2026-07-21 v1.10.86 — Export·Import ZIP은 원본 품질과 서버 메모리를 분리해 다룬다
게시물 Export의 목적은 이미지 품질을 다시 낮추는 것이 아니라 Markdown과 원본 자산을 하나의 이동 가능한 ZIP으로 묶는 것이다. 기존 방식은 모든 자산을 Buffer로 읽고 동기 deflate 결과와 최종 ZIP까지 메모리에 함께 보관해, 설정 가능한 2GB 한도가 실제 서버 메모리 한도로 이어질 수 있었다. Export는 디스크 출력 스트림으로 직접 생성하고 이미 압축된 이미지·영상·첨부 파일은 ZIP `store` 방식으로 담아 원본 바이트를 그대로 유지한다.
Import도 multipart 전체와 압축 해제된 모든 엔트리를 메모리에 보관하지 않는다. 업로드 ZIP은 크기 제한을 적용하며 임시 파일로 스트리밍하고, 중앙 디렉터리의 엔트리 수·개별 크기·총 비압축 크기·압축률을 먼저 검증한다. Markdown은 총 50MB 안에서만 읽고 자산은 필요한 엔트리만 업로드 경로로 스트리밍해 ZIP 폭탄과 메모리 급증을 함께 제한한다.
## 2026-07-21 v1.10.85 — 로그인 세션은 비밀번호와 함께 폐기하고 공개 목록은 요약만 전달한다
서명된 쿠키의 만료 시각만 확인하면 비밀번호를 바꾼 뒤에도 탈취된 기존 세션이 남을 수 있다. 회원 행에 `session_version`을 두고 로그인 토큰에 발급 당시 버전을 포함해, 비밀번호 변경·재설정·관리자 직접 변경 시 버전을 증가시키고 이후 요청에서 DB 값과 비교한다. 배포 시점의 기존 세션은 버전 1로 호환하되 비밀번호가 바뀐 계정의 이전 토큰만 무효화한다.
로그인 실패 제한은 현재 단일 NAS 앱 프로세스 운영을 기준으로 회원·관리자 범위를 분리하고, 계정과 IP를 함께 본다. 계정 원문과 IP 원문은 메모리 키에 남기지 않고 해시하며 15분 동안 계정 5회 또는 IP 20회 실패 시 429를 반환한다. 다중 인스턴스로 확장할 때는 공유 저장소 기반 제한으로 교체한다.
공개 게시물 목록은 화면에 제목·썸네일·요약만 필요하므로 전체 본문을 응답하지 않는다. 서버가 최대 4,000자의 본문 앞부분에서 표시용 요약을 만들고 기본 목록 개수 상한을 항상 적용한다. RSS처럼 본문 기반 요약이 필요한 내부 호출만 명시적으로 본문 조회를 허용한다.
## 2026-07-20 v1.10.81 — AdSense 홈 요약은 API totals와 응답 통화를 기준으로 본다
AdSense Management API의 보고서 응답은 차원 없이 요약을 요청할 때 `rows`보다 `totals`가 실제 합계를 담는 기준이 된다. 관리자 대시보드가 `rows[0]`만 읽으면 공식 AdSense 화면에 수익이 있어도 0으로 보일 수 있으므로, 보고서 합계는 `ReportResult.totals`를 먼저 읽고 필요한 경우에만 첫 row로 보완한다.
또한 수익 통화는 운영자가 임의로 지정한 기본값보다 API 응답 헤더의 `currencyCode`가 공식 화면과 맞을 가능성이 높다. 따라서 금액 표시는 응답 통화 코드를 우선 사용하고, AdSense 홈 화면의 확인 리듬에 맞춰 오늘 현재까지·어제·지난 7일·이번 달·최근 28일·잔고를 한 영역 안에서 보여준다.
## 2026-07-20 v1.10.80 — AdSense 비밀값은 서버에서만 다룬다
관리자 대시보드에 AdSense 수익 통계를 표시하려면 Google OAuth client secret과 refresh token이 필요하다. 이 값은 브라우저에 노출되면 안 되므로 관리자 화면에서 직접 Google API를 호출하지 않고, 서버 전용 `/admin/api/adsense/summary`에서만 환경 변수를 읽어 access token을 발급한다.
AdSense API 실패가 대시보드 전체 장애로 보이면 운영자가 현재 접속자나 자체 통계까지 확인하지 못하므로, API 오류와 설정 누락은 AdSense 카드 내부 안내로 제한한다. 외부 보고서 조회는 오늘·어제·이번 달 세 구간만 요청해 관리자 첫 화면에서 필요한 기념성/운영성 숫자를 가볍게 확인하는 데 집중한다.
## 2026-07-20 v1.10.74 — 페이지 목록은 게시글 목록의 작업 화면 언어를 따른다
게시글과 페이지는 노출 방식은 다르지만 관리자 목록에서 운영자가 하는 일은 검색, 상태 확인, 통계 확인, 수정 진입으로 거의 같다. 따라서 페이지 목록도 게시글 목록과 같은 상단 검색·필터·정렬 구조와 추천/썸네일 열을 사용해 두 화면의 탐색 리듬을 맞춘다. 페이지 전용 서버 페이지네이션은 아직 두지 않고, 현재 API 응답 위에서 클라이언트 필터로 UI 일관성을 먼저 확보한다.
## 2026-07-20 v1.10.73 — 백링크와 이미지 크기는 작성자가 보는 단위에 맞춘다
백링크는 게시글 목록에 표시되지만 실제 링크 출처는 게시글만이 아니라 고정 페이지도 될 수 있다. 운영자가 삭제나 수정 전에 참조 관계를 확인하는 목적이라면 콘텐츠 종류보다 `/post/:slug`를 실제로 가리키는지 여부가 더 중요하므로, 백링크 개수와 참조 모달은 게시물·페이지 본문을 함께 본다.
단일 이미지 크기는 기존 `regular/wide/full` 폭 옵션을 유지하면서 별도 픽셀 너비 `{size=숫자}`를 추가했다. 이렇게 하면 기존 문서와 호환되며, 작성자는 오른쪽 설정 패널에서 원하는 가로 크기를 입력하고 공개 화면은 `max-width`만 제한해 원본 비율을 유지한다.
## 2026-07-08 v1.10.66 — 현재 접속자는 세션보다 사람에 가깝게 묶어 보여준다
실시간 세션 ID는 같은 방문자라도 탭이나 브라우저 저장소 단위가 달라지면 서로 다른 값으로 보일 수 있다. 관리자 화면에서 SID를 그대로 노출하면 사람 식별 보조 정보처럼 보이지만, 실제로는 같은 사람이 여러 페이지를 열었을 때 여러 명처럼 보이는 혼란을 만든다.
따라서 현재 접속자 화면에서는 SID를 제거하고 IP만 보조 정보로 표시한다. 집계는 로그인 사용자는 회원 ID 기준, 익명 방문자는 IP 기준으로 묶어 한 사람이 여러 페이지를 열어도 1명처럼 보이게 하고, 같은 묶음 안의 활성 페이지 수는 `열린 페이지 N개`로 표시한다. IP 기반 묶음은 공유망의 여러 사용자를 완벽히 구분하지 못하지만, 운영 화면에서는 같은 사람의 여러 탭을 여러 방문자로 오해하는 문제를 줄이는 쪽을 우선한다.
## 2026-07-08 v1.10.65 — 관리자 통계 표시는 해석 가능한 범위로 정규화한다
읽음 이벤트와 스크롤 heartbeat는 페이지뷰와 별도 요청으로 수집되므로 네트워크 타이밍, 탭 이동, 과거 집계 방식에 따라 원시 카운터가 `읽음 > 조회`, `75% 스크롤 > 50% 스크롤`처럼 해석하기 어려운 조합이 될 수 있다. 운영 화면에서 이런 원시값을 그대로 보여 주면 실제 방문 흐름보다 집계 오류처럼 먼저 인식된다.
따라서 원본 통계 행은 보존하되 관리자 조회 응답에서는 `읽음 <= 조회`, `100% 스크롤 <= 75% 스크롤 <= 50% 스크롤 <= 조회`로 정규화해 표시한다. 수집 단계의 세션 세분화는 별도 개선 과제로 남겨, 표시 안정성과 원인 분석 가능성을 함께 유지한다.
## 2026-07-08 v1.10.62 — 대시보드 자동 갱신은 사용자의 선택을 덮어쓰지 않는다
대시보드는 30초마다 통계를 다시 조회하지만, 조회 결과가 바뀔 때마다 기본 날짜 선택 로직을 다시 실행하면 사용자가 과거 날짜를 보고 있는 중에도 오늘 통계로 되돌아간다. 차트 막대 클릭은 사용자의 명시 선택이므로 자동 갱신보다 우선해야 한다. 따라서 기본 선택은 초기 진입과 기간 탭 변경 때만 수행하고, 자동 갱신은 현재 선택 범위를 유지한 채 값만 새로 받아오도록 정리한다.
상세 패널 재조회 중 기존 콘텐츠를 로딩 문구 하나로 교체하면 패널 높이가 줄어 화면 위치가 흔들린다. 운영자가 대시보드를 켜둔 상태에서 읽고 있는 위치가 유지되어야 하므로, 재조회 중에는 기존 내용을 그대로 두고 새 응답이 도착했을 때 값만 바꾼다.
## 2026-07-08 v1.10.61 — 현재 접속자 식별은 IP와 세션을 함께 본다
현재 접속자 화면에서 IP만 표시하면 같은 공유망 방문자가 한 사람처럼 보이거나, 같은 사람이 네트워크를 바꾸면 다른 사람처럼 보일 수 있다. 반대로 세션만 표시하면 운영자가 같은 네트워크에서 들어온 방문자인지 빠르게 판단하기 어렵다. 따라서 실시간 세션에는 요청 IP를 보조로 저장하고, 화면에는 IP와 `session_hash` 앞자리 SID를 함께 표시한다. 원문 클라이언트 세션 ID는 노출하지 않고, 방문자 통계 고유성은 기존 해시 기반 집계를 유지한다.
인기 게시물과 인기 페이지는 제목이 운영 판단의 핵심이므로 좌우 2열 카드보다 세로 목록이 더 적합하다. 조회·읽음·최근 30일 조회·작성일·스크롤 도달은 보조 지표라 한 줄 압축 표기로 정리해 제목 폭을 우선 확보한다.
## 2026-07-08 v1.10.60 — 대시보드 통계 상세는 모달보다 본문 패널로 다룬다
통계 차트 막대 클릭 시 모달을 띄우면 하단의 유입·인기 콘텐츠 카드가 어떤 기간을 기준으로 하는지 분리되어 보이고, 일간 선택에서도 오늘인지 선택 날짜인지 해석이 흐려질 수 있다. 따라서 차트는 기간 선택 도구로 두고, 선택 결과는 하단 **선택한 기간 상세** 패널에 직접 표시한다.
검색 유입 키워드는 대형 검색엔진이 referrer query를 대부분 전달하지 않아 자체 통계만으로 안정적으로 제공하기 어렵다. 빈 키워드 영역은 기능 오류처럼 보일 수 있으므로 대시보드에서는 제거하고, 운영 판단에 더 안정적인 유입원·디바이스·인기 게시물·인기 페이지 중심으로 정리한다.
## 2026-07-08 v1.10.56 — 글쓰기 미디어 선택은 하나의 서버 페이지 흐름으로 맞춘다
본문 이미지 삽입 모달은 이미 관리자 미디어 API의 페이지 응답과 검색을 사용하지만, 대표 이미지 선택 모달은 전체 미디어 배열을 받은 뒤 클라이언트에서 원본 이미지만 걸러 쓰는 레거시 방식이 남아 있었다. 미디어가 많아질수록 두 선택 화면의 성능과 검색 결과가 달라질 수 있으므로 대표 이미지 선택도 같은 `/admin/api/media?paged=1&tab=library` 흐름으로 통일했다.
또한 검색 결과 개수에 따라 모달 높이가 계속 변하면 입력 중 화면 기준점이 흔들린다. 글쓰기 미디어 모달은 결과 목록만 내부 스크롤되도록 고정 높이 패널로 다루고, 업로드/라이브러리 탭 전환과 검색 결과 변화가 모달 외곽 크기를 바꾸지 않게 한다.
## 2026-07-06 v1.10.52 — 사이트 용량 카드는 설정 화면에 맞게 간략화한다
설정 화면의 용량 요약은 백업 도구와 별개이므로 `백업 현황` 대신 `사이트 용량`으로 이름을 바꿨다. 파일 용량은 이미지·영상·보내기·합계 4행만 보여 주고, 자동 생성·프로필 이미지는 이미지 행의 보조 설명으로만 표시한다. 큰 파일 목록은 설정과 무관하다고 판단해 제거했다.
# 의사결정 이력
## 2026-07-06 v1.10.51 — 백업 현황은 분석 통계와 분리해 가볍게 집계한다
사이트 설정에서 필요한 현황은 방문자·유입 분석이 아니라 백업과 서버 용량 점검 기준이다. 분석 테이블이나 관리자 미디어 사용처 계산을 재사용하면 불필요하게 무겁고, 미디어 전체 사용처 판정까지 동반되어 설정 화면 진입 비용이 커질 수 있다.
따라서 백업 현황 API는 콘텐츠 수량은 PostgreSQL의 단순 COUNT로, 파일 용량은 `public/uploads` 디렉터리 스캔으로만 집계한다. 업로드 파일은 원본 미디어, 자동 생성 이미지, 프로필 이미지, 내보내기 파일로 나누어 보여 주고, 운영자가 용량 원인을 빠르게 찾을 수 있도록 큰 파일 상위 목록만 함께 제공한다.
## 2026-07-06 v1.10.48 — 이동 오버레이는 로딩 표시가 아니라 화면 전환 막으로 동작해야 한다
게시물 상세를 블로킹 조회로 되돌려도 운영 환경에서는 로딩 스피너 뒤로 홈 헤더 이미지가 다시 보인 뒤 상세로 넘어가는 현상이 남았다. 원인은 로딩 오버레이가 반투명이라 이전 화면이 비쳐 보일 수 있고, `page:loading:end`가 새 화면의 실제 페인트보다 빠르게 발생할 수 있다는 점이었다.
따라서 전역 이동 오버레이는 단순 로딩 표시가 아니라 화면 전환을 완전히 가리는 막으로 다룬다. 배경은 불투명하게 만들고, 해제는 `page:finish` 이후 렌더 프레임을 기다린 다음 수행해 이전 홈 화면의 상단이 다시 노출되는 구간을 줄인다.
## 2026-07-06 v1.10.47 — 게시물 이동은 로딩 후 진입으로 확정한다
즉시 전환, 라우터 스크롤 비활성화, Nuxt `page:finish` 이후 스크롤 적용, 짧은 전환 베일을 차례로 시도했지만 홈에서 게시물로 이동할 때 이전 홈 헤더 이미지가 순간 노출되는 현상이 남았다. 홈 화면의 큰 헤더 이미지와 문서 스크롤, Nuxt 클라이언트 라우팅의 페인트 타이밍이 맞물리는 문제라 즉시 전환을 더 고집하면 코드 복잡도만 커지고 사용자 체감은 안정되지 않는다.
따라서 게시물 상세 이동은 본문을 블로킹으로 먼저 조회하고, 이동 중에는 전역 로딩 오버레이로 이전 화면을 가린 뒤 준비된 상세 화면으로 진입하는 방식을 다시 채택한다. 문서형 사이트에서는 약간의 대기보다 화면이 튀지 않는 안정성이 더 중요하다고 판단했다.
## 2026-07-06 v1.10.46 — 즉시 전환을 유지하되 화면 교체 구간만 가린다
라우터 기본 스크롤을 끄고 Nuxt `page:finish` 이후로 스크롤 적용을 늦춰도, 홈에서 게시물로 이동할 때 이전 홈 화면의 헤더 이미지가 순간적으로 보일 수 있었다. 이는 페이지 컴포넌트 교체와 브라우저 페인트 사이의 짧은 구간에서 발생하는 시각적 문제이므로, 데이터 준비 완료까지 기다리는 방식으로 돌아가기 전에 화면 교체·스크롤 정리 구간만 덮는 전환 베일을 추가한다.
전환 베일은 전체 로딩 오버레이와 다르게 긴 데이터 대기 상태를 표현하지 않는다. 이동 시작부터 새 화면의 스크롤 위치가 정리된 직후까지만 배경색으로 화면을 가려, 즉시 전환 구조는 유지하면서 이전 화면의 상단 노출을 숨긴다.
## 2026-07-06 v1.10.45 — 스크롤 적용은 라우트 확정이 아니라 페이지 렌더 완료를 기준으로 한다
Vue Router의 `afterEach`는 URL과 라우트 상태가 바뀐 직후 실행되지만, Nuxt의 실제 페이지 컴포넌트 교체가 끝났다는 뜻은 아니다. 이 시점에 최상단 스크롤을 적용하면 이전 화면의 헤더 이미지가 먼저 보인 뒤 새 페이지가 붙을 수 있다. 따라서 `afterEach`에서는 이동 정보를 저장만 하고, Nuxt `page:finish` 이후 렌더 프레임을 기다린 뒤 새 화면 기준으로 스크롤을 적용한다.
기본 `scrollBehavior: false`는 유지한다. 스크롤 제어 주체를 하나로 유지하면서 적용 시점만 늦춰, 이전 화면을 움직이지 않고 새 화면의 시작 위치를 정리하는 쪽을 선택했다.
## 2026-07-06 v1.10.44 — 스크롤은 라우트 전환 후 새 화면에서만 적용한다
게시물 상세를 즉시 전환 방식으로 바꿔도 Vue Router 기본 스크롤 처리가 남아 있으면, 새 화면이 그려지기 전에 이전 화면이 먼저 최상단으로 이동하는 순간이 보일 수 있다. 사용자는 이를 페이지가 넘어가기 전에 화면이 깜빡이는 현상으로 느끼므로, 라우터 기본 `scrollBehavior`를 끄고 클라이언트 플러그인에서 라우트 확정 후 렌더 프레임을 기다린 다음 새 화면 기준으로만 스크롤을 적용한다.
이 방식은 기존 스켈레톤 전환 구조를 유지하면서도 스크롤 제어 책임을 분리한다. 해시 링크는 대상 요소가 존재하면 해당 위치로 이동하고, 일반 이동은 새 화면 최상단으로 정리한다.
## 2026-07-06 v1.10.43 — 게시물 이동은 새 화면을 먼저 보여 주고 내부에서 로딩한다
전체 화면 오버레이는 이전 화면의 스크롤 튐을 숨기는 데는 효과적이지만, 문서형 사이트에서는 클릭 후 새 페이지가 바로 열린다는 감각이 더 중요하다. 따라서 게시물 상세의 클라이언트 내부 이동은 라우트를 먼저 전환하고, 아직 상세 데이터가 준비되지 않았을 때는 이전 글을 보여 주지 않고 새 상세 화면 안에서 제목·메타·이미지·본문 자리 스켈레톤을 표시한다.
직접 접속과 검색엔진 수집에는 여전히 SSR 본문 선조회가 필요하므로 서버 렌더링에서는 기존처럼 게시물을 먼저 받아 SEO 메타를 완성한다. 이 방식은 사용자 이동 체감과 SSR 안정성을 함께 유지한다.
## 2026-07-06 v1.10.42 — 이동 중 화면은 이전 화면을 가리고 준비 후 보여 준다
게시물 상세 이동을 블로킹 조회로 되돌려도 브라우저 스크롤 제어 타이밍에 따라 이전 화면이 순간적으로 최상단처럼 보이면 사용자는 화면이 튄다고 느낀다. 이번 수정은 즉시 전환·스켈레톤 방식으로 다시 흔들기보다, 이동 중 전체 화면 로딩 오버레이를 표시해 이전 화면의 중간 상태를 가리고 준비된 다음 화면만 보이게 한다.
글쓰기 이미지 선택 모달은 미디어가 수천·수만 개로 늘어날 수 있으므로 전체 원본 배열을 한 번에 받아 클라이언트에서 필터링하지 않는다. 이미 관리자 미디어 API가 사용처 제목 검색과 페이지 응답을 지원하므로, 모달도 같은 서버 페이지 응답을 사용해 파일명과 게시물·페이지 제목으로 좁히고 현재 페이지 단위로만 표시한다.
## 2026-07-03 v1.10.28 — 공개 검색은 정규화 컬럼과 trigram 인덱스를 쓴다
게시물이 수천 개로 늘면 통합 검색 입력마다 `lower(content)`를 직접 계산하는 방식은 본문 길이에 비례해 비용이 커진다. 공개 검색은 부분 문자열 검색과 한글 검색이 중요하므로 PostgreSQL full-text search보다 `pg_trgm` 인덱스를 먼저 사용한다. 게시물과 페이지에는 업로드 경로·마크다운 이미지 토큰을 제거한 `search_text` 컬럼을 두고, DB 트리거가 저장·수정 시 자동 갱신한다.
애플리케이션 쿼리는 `search_text LIKE '%검색어%'`만 확인해 인덱스가 탈 수 있게 하고, 제목·요약 앞부분 일치 결과를 먼저 보여 준다. 이렇게 하면 공개 검색 결과의 기존 부분 일치 감각은 유지하면서 검색 모달 입력마다 본문 전체를 반복 가공하는 일을 줄일 수 있다.
## 2026-07-03 v1.10.27 — 미디어 사용처 계산은 목록 요약과 분리한다
관리자 글 목록은 성능을 위해 본문을 제외한 요약 응답을 사용한다. 하지만 미디어 사용처 계산은 게시물 본문과 페이지 본문 안의 이미지 URL을 정확히 확인해야 하므로 같은 목록 함수를 재사용하면 본문 이미지가 모두 미사용으로 분류될 수 있다. 따라서 미디어 관리는 목록 UI용 함수가 아니라 미디어 참조 필드만 따로 조회하는 전용 함수를 사용한다.
자동 생성 파생 이미지는 원본 이미지에서 다시 만들 수 있는 시스템 파일이지만, 사용 중이면 원본과 마찬가지로 삭제되면 안 된다. 사용처가 없는 고아 파생 파일만 정리용으로 삭제할 수 있게 두고, 파일명 변경은 원본 연결이 깨질 수 있어 계속 제한한다.
## 2026-07-03 v1.10.26 — 관리자 고정은 공개 추천과 분리한다
비공개 글을 작성 대기 목록처럼 상단에 유지하고 싶은 요구는 공개 홈의 추천 글 노출과 목적이 다르다. 추천(`isFeatured`)을 재사용하면 공개 화면 노출 의도와 관리자 작업 큐가 섞이므로, 게시물에는 관리자 전용 `adminPinned` 플래그를 별도로 둔다. 글 목록은 정렬 방식과 무관하게 고정 글을 먼저 보여 주고, 토글은 목록 행 메뉴에서만 제공해 글쓰기 자동 저장 payload와 충돌하지 않게 한다.
## 2026-07-03 v1.10.25 — 원본 사용 여부는 파생 이미지 사용까지 포함한다
본문 이미지는 공개 화면에서 원본 대신 `body-1600`, `body-720`, `body-2560` 파생 이미지를 사용한다. 하지만 운영자는 원본을 백업·다운로드 기준으로 보관하므로, 원본 URL이 직접 본문에 없다는 이유만으로 미사용으로 분류하면 실제 사용 중인 이미지 묶음을 일괄 삭제할 위험이 생긴다. 따라서 관리자 미디어 사용처 계산은 원본 URL 직접 참조와 함께 원본에서 생성된 카드·페이지 커버·본문 파생 이미지 URL 참조를 원본 사용처로 합산한다. 반대로 원본이 정말 미사용이라 삭제될 때는 연결된 자동 생성 파생 이미지까지 함께 정리해 시스템 파일이 남지 않게 한다.
글쓰기 이미지 선택 모달은 삽입해야 하는 값이 원본 URL이므로 라이브러리에는 원본 파일만 보여 준다. 다만 썸네일 표시는 원본을 내려받을 필요가 없어서, 이미 생성된 720px 본문 파생 이미지나 카드 썸네일을 우선 표시하고 실패했을 때만 원본으로 되돌린다.
## 2026-07-03 v1.10.24 — 글쓰기 저장 기준은 실제 저장 필드만 본다
관리자 글쓰기 폼은 저장 payload에 태그 표시명 보조값(`tagNames`)을 함께 담아 새 태그 이름을 만들 수 있게 한다. 하지만 기존 태그의 표시명은 태그 API가 늦게 도착하면 슬러그에서 실제 이름으로 바뀔 수 있고, 이 값까지 미저장 비교에 포함하면 사용자가 아무것도 하지 않아도 비공개·멤버십 글에서 이탈 경고와 `Update` 활성화가 발생한다. 따라서 미저장 비교는 제목, 본문, 상태, 태그 슬러그처럼 실제 게시물 저장 상태를 바꾸는 필드만 기준으로 한다.
신규 글은 첫 자동 저장이 `POST`로 DB 행을 만든 뒤 편집 URL로 이동한다. 이 사이에 사용자가 내용을 더 입력하면 새 편집 화면이 첫 요청 payload만 받아 최신 입력이 사라질 수 있으므로, 첫 `POST` 직후 현재 폼 payload를 다시 읽고 차이가 있으면 생성된 글에 `PUT`으로 최신 값을 반영한 뒤 이동한다. 이동 중에는 짧은 안내 오버레이를 띄워 첫 초안 저장이 진행 중임을 명확히 한다.
## 2026-07-03 v1.10.23 — 관리자 글 목록은 서버에서 먼저 자른다
하루에 수십 개씩 이전 글을 추가하면 관리자 글 목록은 곧 수천 건 규모가 된다. 목록 화면이 전체 글을 한 번에 받은 뒤 브라우저에서 검색·필터링하면 응답 크기뿐 아니라 태그 필터와 정렬 계산도 글 수에 비례해 커진다. 따라서 글 목록 화면은 `/admin/api/posts?paged=true` 응답을 사용해 서버에서 검색·상태·태그·추천·정렬 필터와 페이지네이션을 먼저 적용한다.
대시보드와 미디어 사용처 계산은 아직 기존 배열 응답을 전제로 하므로 `/admin/api/posts` 기본 응답은 배열 호환을 유지한다. 새 페이지 응답은 명시적으로 `paged=true`일 때만 사용해 기존 화면의 변경 범위를 줄이고, 목록 전용 화면만 `items`, `pagination`, `counts`를 읽도록 분리한다.
## 2026-07-03 v1.10.22 — 관리자 글 목록은 본문 없는 요약 응답을 쓴다
관리자 글 목록은 제목, 상태, 태그, 조회수, 발행일, 썸네일만 빠르게 훑는 화면이다. 게시물 본문 전체를 함께 내려주면 글 수가 늘수록 목록 진입 payload가 본문 길이에 비례해 커지고, 이미지 마크다운이 많은 글에서는 관리 화면 로딩에도 영향을 준다. 따라서 `/admin/api/posts`는 목록 요약 필드만 반환하고, 상세 편집 화면은 기존 `/admin/api/posts/:id`에서 본문을 계속 조회한다.
기존 목록의 추천 토글은 전체 게시물 수정 API에 목록 데이터를 다시 조립해 보내는 구조였다. 목록 응답에서 본문을 제외하면 이 방식은 본문을 빈 값으로 덮어쓸 위험이 있으므로, 추천 상태만 바꾸는 `/admin/api/posts/:id/featured` 전용 API로 분리한다. 목록 검색은 더 이상 본문 전문 검색이 아니라 제목·슬러그·요약·태그 기준으로 유지하고, 본문 검색 고도화는 공개 통합 검색 인덱스 개선 과제와 별도로 다룬다.
## 2026-07-03 v1.10.21 — 관리자 글 목록 필터는 태그 증가를 전제로 한다
관리자 글 목록의 상태·추천·정렬 필터는 선택지가 고정되어 있어 기본 select로 충분하지만, 태그는 운영이 길어질수록 계속 늘어난다. 태그 전체를 네이티브 select에 한 줄로 누적하면 탐색성과 키보드 검색성이 떨어지므로, 글 목록 태그 필터는 검색형 combobox로 바꾸고 현재 사용 중인 태그만 후보로 보여 준다. 검색어가 없으면 전체 태그 옵션과 기존 정렬된 태그 목록을 보여 주고, 입력 중에는 태그명·슬러그 기준으로 좁힌다.
관리자 글 목록의 썸네일은 48×36px 수준의 작은 미리보기만 필요하다. 이미 게시물 API가 원본 대표 이미지와 카드 썸네일 URL을 함께 제공하므로, 목록 화면에서는 `featuredImageThumbnail`을 우선 사용해 관리자 진입 때마다 원본 이미지를 반복 다운로드하는 부담을 줄인다. 썸네일이 아직 없으면 기존 원본 fallback은 유지한다.
## 2026-07-03 v1.10.20 — 글쓰기 태그 추천은 최근 작업 흐름을 우선한다
게시물 작성자는 비슷한 주제의 글을 연속으로 작성하는 경우가 많다. 기존 Tags 입력은 빈 상태에서 메인 태그만 보여 주고 일반 태그는 검색해야 했기 때문에, 같은 태그를 반복해서 쓰는 작업에서 매번 검색이 필요했다. 이미 관리자 태그 API가 공개 발행글 기준 `lastUsedAt`을 제공하므로 별도 테이블을 만들지 않고 최근 사용 태그 5개를 추천 목록 상단에 배치한다. 검색어가 있을 때는 기존처럼 전체 태그 이름·슬러그 검색을 유지한다.
관리자 미디어의 표시 개수 선택은 기능상 같은 셀렉트인데 기본 브라우저 화살표가 보여 다른 관리자 셀렉트와 어긋났다. 해당 셀렉트도 `appearance-none`과 SVG chevron을 쓰는 기존 관리자 폼 패턴으로 맞춰 화면 언어를 통일한다.
## 2026-07-02 v1.10.19 — 구조 블록 안의 기본 마크다운도 작성 화면에서 보여 준다
인용·콜아웃·토글은 문서 안의 보조 구조지만, 내부에 선택지 목록이나 강조 링크가 들어가는 경우가 많다. 공개 화면과 라이브 작성 화면의 차이가 커지면 작성자는 저장 후 결과를 다시 확인해야 하므로, 기본 굵게·링크·색상뿐 아니라 불릿/번호 목록까지 라이브 표시와 저장 왕복을 맞춘다. 다만 복잡한 중첩 블록 편집기는 별도 구조가 필요하므로 이번 범위는 기본 목록과 인라인 문법에 한정한다.
기본 목록을 라이브 편집용 HTML로 바꾸면 브라우저 DOM의 커서 위치와 실제 저장되는 `- 항목` 원문 줄의 길이가 달라진다. 방향키 이동과 줄 삭제는 원문 줄 기준으로 작동해야 하므로 contenteditable offset 계산도 목록 마커와 항목 사이 줄바꿈을 포함하도록 맞춘다. 공개 토글은 닫힌 상태에서 chevron만 눌러야 하는 구조가 명확하지 않으므로 제목 클릭도 펼침·접힘 트리거로 포함한다.
게시물을 비공개나 초안에서 공개로 직접 바꿀 때 발행 시각이 비면 공개 정렬과 예약 판별이 불안정해진다. 발행 모달 경로뿐 아니라 상태 셀렉트와 API 직접 요청에서도 같은 결과가 나오도록 클라이언트와 서버 양쪽에서 빈 발행 시각을 현재 시각으로 보정한다.
## 2026-07-02 v1.10.18 — 미디어 관리는 API 응답부터 나눠 받는다
v1.10.16에서 관리자 미디어 화면 렌더링은 50·100·200개 단위로 나눴지만, API는 여전히 전체 미디어 배열을 내려줄 수 있었다. 미디어가 수백 개 이상으로 늘면 화면 렌더링을 나눠도 네트워크 payload와 클라이언트 메모리 부담이 남는다. 따라서 미디어 관리 화면은 `paged=1` 응답을 사용해 탭·폴더·종류·미사용·검색 조건이 적용된 현재 페이지 항목과 전체 개수 메타만 받는다. 글쓰기 이미지 선택과 대표 이미지 선택은 원본 전체 선택 흐름이 아직 필요하므로 기존 배열 응답을 유지해 호환성을 우선했다.
## 2026-07-02 v1.10.17 — 콜아웃도 일반 문단과 같은 인라인 조합을 쓴다
콜아웃은 안내 문맥 안에서 게시물 내부 링크를 강조하는 경우가 많다. `**[링크](url)**`처럼 굵은 링크를 쓰면 기존 인라인 렌더러가 굵게 세그먼트 내부를 문자열로만 출력해 링크가 동작하지 않았다. 따라서 굵게·기울임·색상 세그먼트도 내부 인라인 문법을 재귀 렌더링하도록 맞추고, 콜아웃 라이브 편집 본문도 인용과 같은 멀티라인 인라인 렌더러를 사용하게 했다.
## 2026-07-02 v1.10.16 — 라이브 작성과 미디어 관리는 표시 상태를 신뢰 기준으로 맞춘다
관리자 라이브 모드는 공개 렌더링과 작성 편의 사이에 있지만, 인용문 안의 색상·굵게 토큰이 그대로 보이면 작성자는 실제 공개 화면을 예측하기 어렵다. 따라서 인용 멀티라인 편집도 일반 인라인 렌더러를 사용해 색상·굵게 같은 문법을 표시한다. 토글은 편집 편의를 위해 항상 펼치는 것보다 저장된 기본 접힘 상태를 확인할 수 있어야 하므로, 라이브 모드 진입 시 선언 줄의 기본 상태를 우선 반영한다.
미디어 화면은 파일이 수백 개로 늘어나는 운영을 전제로 한다. 서버 응답 구조는 유지하되 관리자 화면 렌더링을 50·100·200개 단위로 나누고, 원본 라이브러리의 이미지 카드도 가능한 작은 파생 이미지를 우선 사용해 브라우저가 한 번에 고용량 원본을 많이 내려받는 상황을 줄인다. 상세 모달과 다운로드는 운영자 확인·백업 목적이 있으므로 원본 접근을 계속 유지한다.
## 2026-07-02 v1.10.15 — 본문 파생 이미지는 해상도형 파일명을 쓴다
`body`와 `lightbox` 같은 용도명 파일명은 관리자 미디어에서 실제 크기를 바로 판단하기 어렵다. 본문 이미지는 관리자가 용량과 사용처를 자주 확인하는 자산이므로 새 생성 규칙을 `body-1600`, `body-720`, `body-2560` 해상도형 이름으로 바꾼다. 이미 운영 볼륨에 생성된 `body`, `lightbox` 파일은 삭제하지 않고 레거시 본문 이미지로 계속 분류해 사용처 연결과 삭제 잠금을 유지하며, 백필 명령으로 새 해상도형 파일을 생성할 수 있게 한다.
## 2026-07-02 v1.10.14 — 재생성 가능한 이미지는 즉시 재검증한다
게시물 파생 이미지는 원본 업로드와 달리 같은 URL의 파일 내용을 다시 만들 수 있다. 기존 `max-age=3600` 캐시는 평소 성능에는 유리하지만, 한 번 깨진 파생 이미지 응답이 브라우저에 저장되면 파일을 재생성해도 같은 URL이라 화면에서는 계속 깨져 보일 수 있다. 따라서 `thumbs` 아래 자동 생성 WebP는 `no-cache`로 매번 서버 재검증을 거치게 하고, 공개 본문과 관리자 미디어 미리보기에는 캐시 버전 쿼리를 붙여 이미 저장된 깨진 브라우저 캐시도 우회한다.
## 2026-07-02 v1.10.13 — 글쓰기 미디어 선택은 원본만 허용한다
본문 표시용·라이트박스용·카드용·Pages 커버용 WebP는 공개 화면 최적화를 위한 시스템 파생 파일이다. 이 파일이 글쓰기 미디어 선택창에 노출되면 관리자가 원본 대신 파생 파일을 본문에 삽입할 수 있고, 파생 파일 생성이 실패한 경우 공개 화면도 깨진 URL을 직접 참조하게 된다. 따라서 글쓰기와 대표 이미지 선택창은 원본만 보여 주고, 서버 저장 검증도 자동 생성 이미지 URL을 대표/커버 이미지로 받지 않는다. 파생 파일은 임시 파일에 먼저 만들고 읽기 검증 후 교체해, 실패한 산출물이 남아 원본 fallback을 방해하지 않게 한다.
## 2026-07-02 v1.10.12 — Pages 커버는 별도 3:4 썸네일을 사용한다
홈 Pages 진열장은 섹션별로 여러 페이지 커버가 동시에 노출될 수 있어 원본 이미지를 그대로 사용하면 첫 화면 전송량이 커진다. 게시물 카드 썸네일은 16:9라 Pages 커버의 3:4 비율과 맞지 않으므로, 페이지 커버 전용 `thumbs/*-page-cover.webp` 파생 이미지를 450×600으로 생성한다. 원본은 관리자 다운로드와 보관용으로 유지하고, 공개 홈과 페이지 순서 모달은 생성된 커버 썸네일을 우선 사용한다. 자동 생성 파일은 관리자 미디어의 원본 라이브러리에서 분리해 실수로 미사용 삭제되는 상황을 막는다.
## 2026-07-01 v1.10.11 — 업로드 파일 캐시는 원본과 파생 이미지를 다르게 둔다
업로드 파일은 대부분 고유 URL로 저장되어 같은 URL의 파일 내용이 바뀌지 않는다. 이런 원본·시스템·회원 썸네일 파일은 장기 immutable 캐시를 적용해 이미지 많은 페이지의 반복 요청 비용을 줄인다. 반면 게시물 `thumbs` 파생 이미지는 관리자 미디어에서 같은 URL로 다시 생성할 수 있으므로 1년 immutable을 적용하면 수정 결과가 오래 보이지 않을 수 있다. 따라서 파생 이미지는 1시간 캐시와 `stale-while-revalidate`를 사용하고, 모든 업로드 응답에 `ETag`와 `Last-Modified`를 제공해 재검증 시 304로 빠르게 응답하도록 정리한다.
## 2026-07-01 v1.10.10 — 본문 파생 이미지는 원본 라이브러리에서 숨긴다
본문 이미지 최적화를 위해 하나의 원본에서 1600px 표시용, 720px 표시용, 2560px 라이트박스용 WebP가 생성된다. 이 파일들은 사용자가 직접 고른 원본이 아니라 원본 본문 이미지에 딸린 시스템 파생 파일이므로, 미디어 라이브러리의 원본 목록과 미사용 일괄 삭제 흐름에 섞이면 관리자가 실수로 삭제하기 쉽다. 따라서 관리자 미디어는 세 파생 파일을 모두 본문 이미지 탭으로 분류하고, 서버 사용처 계산도 세 파일을 원본 본문 이미지 사용처에 연결한다. 파일명 자체를 해상도형으로 바꾸는 작업은 기존 운영 볼륨 정리까지 필요하므로 별도 과제로 둔다.
## 2026-07-01 v1.10.9 — 업로드 묶음 제한은 운영자가 설정에서 조절한다
파일별 최대 크기는 서버 운영 정책에 가까워 환경 변수로 유지하지만, 한 번에 몇 개·총 몇 MB까지 올릴지는 운영자가 콘텐츠 작업량에 따라 자주 조절할 수 있다. NAS 설정 파일을 매번 수정하는 방식은 관리 부담이 커서, 사이트 설정 POST 설정 안에 1회 업로드 파일 수와 전체 용량을 추가했다. 서버는 최종 검증을 담당하고, 관리자 미디어·글쓰기 화면은 같은 설정값으로 사전 오류를 보여 준다. 다만 현재 multipart 파서는 요청 본문을 메모리에 받은 뒤 검사하므로, 더 큰 규모의 업로드 안정화는 스트리밍 파서 도입을 별도 과제로 둔다.
## 2026-07-01 v1.10.8 — 라이트박스도 원본 대신 큰 파생 이미지를 우선한다
본문의 크게 보기 기능은 원본 파일 자체를 내려받게 하는 것보다 독자가 이미지를 선명하게 확인할 수 있는 크기를 안정적으로 제공하는 쪽이 목적에 맞다. 따라서 라이트박스에는 2560px WebP 파생 이미지를 우선 사용하고, 별도 원본 보기 UI는 두지 않는다. 파생 이미지가 아직 생성되지 않았거나 로드에 실패한 경우에만 화면 깨짐을 막기 위해 원본 fallback을 유지한다.
## 2026-07-01 v1.10.7 — 본문 이미지는 720px와 1600px를 함께 제공한다
본문 이미지가 모두 1600px 표시용 WebP만 사용하면 모바일 화면이나 갤러리 작은 셀에서도 필요 이상의 이미지를 내려받을 수 있다. 원본은 라이트박스용으로 유지하고, 공개 본문 표시에는 720px와 1600px WebP를 `srcset`으로 함께 제공해 브라우저가 화면 폭에 맞는 파일을 고르게 한다. 기존 게시물은 백필 명령이 두 파생 이미지의 누락 여부를 함께 확인하도록 정리한다.
## 2026-07-01 v1.10.6 — 운영 백필은 NAS 호스트 npm에 의존하지 않는다
NAS 배포는 Docker 이미지 안에서 Node 런타임을 실행하므로 호스트에 npm이 없을 수 있다. 이미지 백필 역시 호스트 npm 명령이 아니라 `sh` 래퍼가 실행 중인 앱 컨테이너의 Node로 처리해야 운영 절차와 맞다. 또한 Nuxt 빌드 산출물에는 source repository 파일이 그대로 포함되지 않으므로, 백필 스크립트는 DB를 직접 조회하고 운영 이미지의 `.output/server` 아래에 필요한 최소 스크립트와 이미지 유틸만 포함하는 방식으로 정리한다.
## 2026-07-01 v1.10.5 — 기존 본문 이미지는 백필 명령으로 일괄 전환한다
본문 표시용 WebP 이미지는 새로 저장되는 게시물에는 자동 생성되지만, 운영 DB에 이미 있는 게시물까지 관리자 화면에서 개별 처리하는 것은 글 수가 늘수록 비현실적이다. 따라서 기존 게시물 본문을 스캔해 누락된 `thumbs/*-body.webp`만 생성하는 백필 명령을 별도로 두고, 운영에서는 배포 후 필요할 때 한 번 실행하는 방식으로 정리한다.
## 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로 고정하고, 저장 중 생긴 추가 변경은 저장이 끝난 뒤 다시 자동 저장되게 한다. 발행 저장 중에는 미저장 변경으로 간주해 라우트 이탈 확인을 유지한다.
## 2026-07-01 v1.9.34 — 인라인 문법은 링크와 색상 안에서도 같은 규칙으로 렌더링한다
Markdown 표 셀과 라이브 미리보기는 같은 인라인 렌더러를 공유하므로, 링크 텍스트나 색상 토큰 내부에 굵게·기울임·줄바꿈 같은 문법이 들어가도 공개 화면과 관리자 화면이 같은 결과를 보여야 한다. 따라서 링크 텍스트를 단순 문자열로 출력하지 않고 다시 인라인 세그먼트로 렌더링하며, ` ` 표기도 멀티라인 인라인 변환의 줄바꿈 입력으로 정규화한다.
## 2026-07-01 v1.9.30 — Pages 추천 표시는 게시물과 같은 아이콘 배지로 맞춘다
홈 Pages 진열장의 추천 상태가 텍스트 `추천` 배지로 표시되면 게시물 카드의 추천 표시와 시각 언어가 달라진다. 추천 상태는 빠르게 인지하는 표시이므로, 게시물과 같은 SVG 아이콘 배지로 통일해 홈 안의 추천 표현을 일관되게 유지한다.
## 2026-07-01 v1.9.28 — HTML 문서 입력은 태그 내부 따옴표를 정규화한다
페이지 HTML 문서 모드는 원문 HTML을 그대로 공개 응답하므로 관리자 입력에 스마트 따옴표가 섞이면 브라우저가 `href`나 `class` 같은 속성을 의도와 다르게 해석할 수 있다. 본문 텍스트의 표현은 건드리지 않고 태그 내부의 따옴표만 저장 전에 정규화해, 붙여넣기 출처에 따른 HTML 파손을 줄인다.
## 2026-07-01 v1.9.27 — 캐러셀 포인터 캡처는 드래그 확정 뒤에만 적용한다
홈 Featured와 Pages 선반은 포인터 다운 시점에 스크롤 트랙이 포인터 캡처를 가져가면 실제 이동이 없는 일반 클릭도 링크 활성화 흐름이 깨질 수 있다. 따라서 포인터 이동이 임계값을 넘은 뒤에만 포인터 캡처를 적용해, 단순 클릭은 링크 이동으로 처리하고 드래그 확정 후에만 카드 밖 이동 추적을 강화한다.
## 2026-07-01 v1.9.26 — 캐러셀 클릭과 드래그는 이동 거리로 구분한다
홈 Featured와 Pages 선반은 링크 카드 위에서 드래그 이동도 지원하므로, 포인터를 누른 즉시 기본 동작을 막으면 일반 클릭까지 링크 이동이 되지 않는다. 따라서 포인터 이동이 임계값을 넘은 경우에만 드래그로 전환하고, 단순 클릭은 브라우저 링크 동작을 그대로 허용한다. 오른쪽 사이드 구분선은 sticky 사이드바가 아니라 레이아웃 컬럼에서 그려 긴 게시물에서도 문서 높이만큼 이어지게 한다.
## 2026-07-01 v1.9.25 — 검색은 IME 조합 중 표시값도 따라간다
한글 IME에서 마지막 글자가 아직 밑줄 상태이면 사용자는 완성된 검색어를 보고 있어도 브라우저는 조합 중으로 유지할 수 있다. 조합 중 검색 요청을 완전히 멈추면 `소문`처럼 화면에는 두 글자가 보여도 결과는 직전 확정값인 `소`에 머문다. 따라서 통합 검색은 조합 중에도 입력창의 현재 표시값을 디바운스로 반영하고, 조합 종료 시 최종 값을 즉시 확정한다.
## 2026-07-01 v1.9.24 — 검색은 IME 조합 확정값만 사용한다
한글 입력 중 조합 중간값을 즉시 검색에 반영하면 사용자는 `에델`을 입력했다고 느껴도 실제 요청에는 `에데` 같은 미완성 값이 들어갈 수 있다. 통합 검색은 입력 즉시 반응해야 하지만 조합 중간 상태는 결과 품질보다 혼란을 만들기 쉬우므로, IME 조합 중에는 검색 요청을 멈추고 조합 종료 후 최종 입력값을 공백·Unicode 정규화한 값으로 조회한다.
## 2026-06-30 v1.9.23 — 누적 로컬 변경은 기능 묶음 단위로 일괄 반영한다
페이지 섹션 관리, 홈 Pages 진열장 노출, 공개 목록 표시개수처럼 같은 주제의 변경이 여러 파일에 걸쳐 누적된 상태에서는 부분 커밋보다 기능 묶음 단위로 한 번에 반영하는 편이 추적이 쉽다. 이번 반영은 누적된 로컬 변경을 기능 맥락 그대로 커밋해 코드·문서·버전 상태를 동기화한다.
## 2026-06-30 v1.9.22 — 홈 선반 화살표 스타일은 섹션 간 일관성을 우선한다
같은 홈 진열장 패턴인 Featured와 Pages의 좌우 이동 버튼 모양이 다르면 기능은 같아도 다른 컴포넌트처럼 보인다. 따라서 Pages 선반 화살표를 Featured와 같은 텍스트형 버튼 스타일로 맞춰, 섹션 제목 오른쪽 네비게이션의 시각 규칙을 통일한다.
## 2026-06-30 v1.9.21 — Pages 선반은 고정 폭 안에서만 스크롤한다
Pages 진열장은 카드가 많아질수록 레일이 부모 grid item의 최소 너비를 밀어 넓힐 수 있다. 이 경우 화면 밖에 카드가 보여도 실제 스크롤 컨테이너 overflow가 생기지 않아 드래그와 좌우 버튼 상태 계산이 맞지 않는다. 따라서 선반 wrapper에 `min-w-0`과 `overflow-hidden`을 주고, 레일은 `w-full max-w-full` 안에서만 가로 스크롤되도록 제한한다.
## 2026-06-30 v1.9.20 — Latest 더보기는 Latest 문맥 안에 둔다
홈 `Posts 더보기` 링크는 Latest 표시 개수가 제한될 때 전체 게시물 목록으로 이어지는 보조 탐색이다. Pages 진열장 아래에 있으면 페이지 섹션의 다음 행동처럼 보이고, Pages가 없는 구성에서는 노출되지 않을 수 있으므로 Latest 목록 렌더링 직후에 배치한다.
## 2026-06-30 v1.9.19 — Pages 진열장은 실제 overflow 기준으로 조작 상태를 갱신한다
Pages 진열장은 이미지 로딩과 반응형 폭 변화 이후에야 실제 스크롤 가능 여부가 확정된다. 최초 렌더 시점의 `scrollWidth`만 기준으로 버튼 상태를 잡으면 화면을 넘어가도 버튼이 비활성처럼 보일 수 있으므로, 선반별 스크롤 컨테이너와 카드 크기를 `ResizeObserver`로 감시해 상태를 계속 갱신한다. 또한 드래그 중 포인터가 카드 바깥으로 이동해도 조작이 끊기지 않도록 문서 레벨 포인터 추적으로 바꾼다. 페이지 순서 변경 모달은 공개 홈의 추천 우선 정렬과 같은 규칙을 즉시 적용해 저장 전 화면과 실제 반영 결과가 어긋나지 않게 한다.
## 2026-06-30 v1.9.18 — 홈 진열장 드래그는 카드 탐색을 우선한다
홈 Featured와 Pages 진열장은 링크 카드 위에서 드래그하는 구조라 이미지의 기본 브라우저 드래그가 먼저 잡히면 캐러셀 조작이 끊긴다. 따라서 트랙 안의 이미지 기본 드래그와 텍스트 선택을 막고, 포인터 이동은 작은 거리부터 스크롤에 반영한다. 스냅은 반 칸 이상 이동했을 때만 다음 카드로 넘어가는 방식보다 작은 의도도 읽히는 임계값 기반으로 처리해, 손을 뗐을 때 가까운 카드 위치로 자연스럽게 붙게 한다.
## 2026-06-30 v1.9.17 — 공개 목록 탐색은 페이지 전환 맥락을 유지한다
게시물 목록과 태그 목록에 서버 페이지네이션을 적용하면 같은 화면 안에서 목록 데이터만 바뀌기 때문에, 스크롤 위치가 하단에 남아 있으면 새 페이지의 첫 게시물을 놓칠 수 있다. 따라서 페이지 번호가 바뀔 때 문서 상단으로 이동해 일반 페이지 이동과 같은 감각을 유지한다. 홈 Latest는 설정된 개수만 보여 주므로 전체 목록으로 이어지는 링크를 하단에 두고, 썸네일 placeholder는 이미지 유무와 관계없이 같은 영역을 차지하게 해 목록 높이를 안정화한다. 페이지 섹션 순서 모달은 공개 홈과 같은 추천 우선 정렬을 보여 주어 관리 화면과 실제 노출 차이를 줄인다.
## 2026-06-30 v1.9.16 — 공개 목록은 설정 기반 페이지네이션으로 제한한다
게시물이 늘어날수록 홈 Latest, 게시물 목록, 태그 상세가 전체 게시물을 한 번에 받아 프론트에서 필터링하면 초기 로딩과 메모리 사용량이 커진다. 따라서 사이트 설정의 POST 설정에서 공개 목록별 표시 개수를 관리하고, 홈은 제한된 최신·추천 글만 받으며 `/posts`와 `/tag/:slug`는 서버 페이지네이션을 사용한다. 태그 전체 화면도 게시물 전체 조회 대신 태그 API의 공개 게시물 수를 사용해 목록 화면의 데이터 규모를 안정적으로 유지한다.
## 2026-06-30 v1.9.15 — 홈 진열장과 관리 모달은 카드 기반 탐색으로 맞춘다
홈 Pages 진열장은 카드가 5개를 넘으면 스크롤바가 숨겨진 상태에서 접근성이 떨어지고, hover 상승 효과가 가로 스크롤 영역에 잘릴 수 있다. Featured와 같은 이전·다음 버튼과 드래그 스냅을 적용해 섹션이 여러 개여도 각 선반을 독립적으로 탐색하게 한다. 관리자 페이지 순서 변경도 단순 행 목록보다 실제 홈 노출 형태에 가까운 카드형 가로 목록이 판단하기 쉬우므로, 썸네일 카드로 순서를 조정하고 같은 모달 안에서 섹션 연결 제거까지 처리한다.
## 2026-06-30 v1.9.14 — 네비게이션 관리의 추가·저장 흐름을 탭 간 통일한다
같은 네비게이션 관리 화면 안에서 어떤 탭은 빈 행을 즉시 추가하고, 어떤 탭은 모달에서 정보를 받은 뒤 추가하며, 페이지 섹션은 행별 저장과 상단 저장 버튼이 함께 보이면 저장 대상이 모호해진다. 따라서 모든 추가 액션은 모달 입력 후 목록에 반영하는 방식으로 맞추고, 저장 버튼은 메뉴 항목용 `네비게이션 저장`과 페이지 섹션용 `페이지 섹션 저장`으로 분리한다. 페이지 섹션 순서도 다른 네비게이션 목록과 같이 드래그앤드롭으로 통일한다.
## 2026-06-30 v1.9.13 — 페이지 섹션과 페이지 순서는 네비게이션 구성으로 관리한다
페이지 섹션은 개별 페이지의 본문 작성 정보보다 홈 Pages 진열장의 네비게이션 구조에 가깝다. 페이지 목록 상단에서 섹션을 관리하면 페이지 수가 늘어날수록 목록 관리와 홈 구성 관리가 섞이므로, 섹션 생성·이름 변경·섹션 순서·섹션 내부 페이지 순서를 관리자 네비게이션 화면으로 옮긴다. 페이지 작성 화면은 기존 섹션을 검색해 선택하는 역할만 맡겨 콘텐츠 작성 흐름을 가볍게 유지한다.
## 2026-06-30 v1.9.12 — 페이지 섹션은 콘텐츠 입력과 분리해 관리한다
페이지마다 섹션명을 직접 입력하면 같은 섹션의 표기 흔들림과 홈 진열장 순서 관리 문제가 생긴다. 따라서 페이지 자체에는 선택된 섹션명을 저장하되, 섹션 생성·이름 변경·배치 순서는 `page_listing_sections` 관리 테이블과 관리자 페이지 목록의 섹션 관리 카드에서 다룬다. 페이지 작성 화면은 등록된 섹션 중 하나를 선택하는 역할만 맡겨 콘텐츠 작성과 홈 구성 관리를 분리한다.
## 2026-06-30 v1.9.11 — 페이지는 독립 화면을 유지하고 노출 메타만 추가한다
페이지를 게시물 피드에 그대로 섞으면 시간순 글 목록의 의미가 흐려질 수 있다. 대신 페이지는 기존처럼 `/pages/:slug` 독립 전체 화면으로 유지하고, 필요한 페이지에만 홈 표시·추천 표시·섹션명·목록 카드 메타를 부여해 섹션별 Pages 진열장과 검색 결과에서 발견할 수 있게 한다. 이렇게 하면 페이지의 랜딩/문서 성격과 게시물 피드의 시간 흐름을 함께 지킬 수 있다.
## 2026-06-30 v1.9.10 — 관리자 세션은 공개 통계에서 제외한다
운영자가 글 작성·수정·확인을 위해 공개 게시물과 페이지를 반복해서 열면 실제 방문자 행동보다 관리자 검수 동작이 조회수와 체류 통계에 크게 반영된다. 따라서 관리자 세션 쿠키가 있는 요청은 공개 경로라도 페이지뷰, 읽음, heartbeat, HTML 문서 페이지 직접 집계에서 제외해 통계가 방문자 기준에 더 가깝게 유지되도록 한다.
## 2026-06-30 v1.9.9 — 관리자 글·페이지 목록의 공개 상태와 보기 액션을 맞춘다
게시물과 페이지는 공개 URL을 가진 같은 콘텐츠 관리 대상이므로 목록에서 상태 용어가 다르면 운영자가 서로 다른 의미로 오해할 수 있다. 따라서 공개 상태 표기는 `공개`로 통일하고, 공개 화면 확인은 제목 편집 링크와 분리해 주소 줄 오른쪽의 외부 링크 아이콘으로 제공한다.
## 2026-06-30 v1.9.8 — 게시물과 페이지 슬러그 규칙을 통일한다
게시물과 페이지는 공개 URL 안에서 같은 성격의 식별자로 쓰이므로 입력 허용 범위가 다르면 운영자가 화면마다 다른 규칙을 기억해야 한다. 따라서 두 관리자 폼과 서버 검증 모두 영문 소문자, 숫자, 한글, 하이픈, 언더바를 같은 방식으로 허용한다. 하이픈과 언더바는 URL 가독성을 위한 구분자로 유지하되 앞뒤 끝에는 두지 않는 규칙을 유지한다.
## 2026-06-29 v1.9.0 — 대시보드 추이는 서버 버킷 집계로 전환한다
대시보드 차트에서 30일 이상을 프론트엔드에서 임의 일수로 다시 묶으면 주간의 일요일~토요일 기준, 이번 주·이번 달·올해의 부분 집계 기준을 명확히 맞추기 어렵다. 따라서 `GET /admin/api/analytics/summary`가 `period`를 받아 일간·주간·월간·년간 버킷을 서버에서 직접 만들고, 화면은 응답된 `startDay`·`endDay`·`label`을 그대로 표시한다. 년간은 빈 과거 연도를 과하게 보여 주지 않도록 실제 집계 시작 연도부터 올해까지 최대 8년만 표시한다.
## 2026-06-29 v1.8.9 — 게시물별 유입은 새 집계로 분리한다
기존 `post_analytics_daily`에는 게시물별 조회·방문자·읽음·체류·스크롤이 이미 있으므로 글 목록 누적 조회수와 게시물별 기본 통계는 즉시 계산할 수 있다. 하지만 `analytics_traffic_daily`는 사이트 전체 유입 집계라 특정 게시물로 나눌 수 없다. 게시물별 유입을 원문 referrer 저장 없이 유지하려면 별도 축약 집계가 필요하므로 `post_analytics_traffic_daily`를 추가하고, 마이그레이션 적용 이후 새 페이지뷰부터 게시물 ID 기준 유입·디바이스·키워드를 함께 누적한다.
## 2026-06-29 v1.8.8 — 통계 추이 상세는 기존 일별 집계를 재사용한다
대시보드 차트 막대는 이미 일자 또는 묶음 구간을 알고 있으므로 별도 원본 로그를 저장하지 않고 `site_analytics_daily`, `analytics_traffic_daily`, `post_analytics_daily`, `page_analytics_daily`를 같은 날짜 범위로 다시 집계한다. 원문 referrer나 검색어를 새로 보관하지 않는 기존 개인정보 최소화 원칙은 유지하며, 검색 키워드는 검색엔진이 referrer query로 전달한 값만 표시한다.
## 2026-06-26 v1.8.7 — 태그 헤더는 이미지 우선 옵션으로 둔다
태그 페이지는 기본적으로 태그 이름과 설명을 보여 주지만, 특정 태그는 시리즈나 작품 단위로 별도 분위기를 줄 필요가 있다. 태그 헤더 이미지가 설정된 경우에는 기본 제목·설명을 자동으로 숨기고 이미지를 우선 표시한다. 오버레이 제목·본문은 선택 입력으로 두어, 이미지만 쓰는 태그와 이미지 위 텍스트를 쓰는 태그를 같은 구조에서 처리한다.
## 2026-06-26 v1.8.6 — 공개 태그 메타 조회 범위를 분리한다
왼쪽 사이드바와 `/tags` 목록은 기존처럼 메인 태그만 노출해야 하지만, `/tag/:slug` 상세와 게시물 배지는 일반 태그의 이름·설명·색상도 필요하다. 따라서 공개 태그 API의 기본값은 메인 태그로 유지하고, `tagType=all` 옵션을 추가해 필요한 화면에서만 전체 태그 메타를 조회하도록 정리했다.
## 2026-06-26 v1.8.1 — 게시물 HTML은 범용 블록으로 분리한다
게시물 수가 아직 적어 기존 `:::embed` 값을 자동 변환·호환하는 코드보다 운영자가 직접 새 형식으로 고치는 편이 단순하다. 작성 기능은 `/html`, `/link`, `/iframe`으로 분리하고 저장 형식도 각각 `:::html`, `:::link`, `:::iframe`로 명확히 둔다. `/html`은 표처럼 마크다운으로 표현하기 어려운 일반 HTML을 표시하되, 게시물 본문에서 실행성 코드가 퍼지지 않도록 script, iframe, 이벤트 속성 등은 제거한다. iframe은 별도 `/iframe` 블록에서 안전 속성 기반 sandbox 렌더링을 사용한다.
## 2026-06-22 v1.7.7 — 장기 방문 통계는 일별 집계를 한 번에 합산한다
Homepage에서 최근 7일·30일·6개월·1년 방문자를 선택해 볼 수 있도록 기간별 평면 필드를 제공한다. 장기 범위 추가로 쿼리 횟수가 늘지 않게 `site_analytics_daily`의 최근 365일 행을 한 번 읽고 PostgreSQL `FILTER` 집계로 각 기간을 동시에 계산한다. 6개월은 대시보드의 기간 기준과 맞춰 최근 180일로 정의하고, 개인용 Homepage의 권장 갱신 주기는 5분으로 둔다.
## 2026-06-22 v1.7.6 — 공개 블로그 통계는 평면 숫자 필드로 제공한다
Homepage Custom API는 운영자가 `mappings`에서 필요한 필드만 선택하는 구조이므로 오늘·누적 방문과 콘텐츠 통계를 중첩 객체가 아닌 평면 숫자 필드로 제공한다. 게시글·댓글·페이지 수는 방문자가 실제로 볼 수 있는 공개 항목만 집계하고, 회원은 개인정보 없이 전체 계정 수만 제공한다. 누적 방문자는 영구 사용자 식별값이 아니라 날짜별 순방문자 수의 합계이므로 같은 방문자가 다른 날짜에 다시 포함될 수 있음을 사용 가이드에 명시한다.
## 2026-06-17 v1.6.2 — 추천 사이트 파비콘은 서버 캐시와 월간 갱신으로 둔다
방문자마다 Google 파비콘 프록시를 호출하면 외부 요청이 누적되고 캐시 주기도 통제하기 어렵다. 추천 사이트 파비콘은 서버가 Google faviconV2에서 한 번 받아 `/uploads/system/`에 저장하고, 공개 화면은 그 로컬 URL을 우선 사용한다. 자동 갱신은 매월 1일 03:00(KST)에 전체 추천 사이트를 대상으로 하고, 운영자는 관리자 추천 사이트 화면에서 수동 새로고침할 수 있다. 수동 썸네일 URL은 기존처럼 최우선이며, 관리자 화면에는 현재 적용될 아이콘 미리보기 열을 둔다.
## 2026-06-17 v1.6.1 — 외부 파비콘은 domain 대신 origin 기반 faviconV2를 쓴다
추천 사이트·북마크 카드의 파비콘은 Google `s2/favicons?domain=`로 호스트만 넘기고 있었는데, `uniare.sori.studio`처럼 서브도메인·신규 사이트에서는 Google이 실제 파비콘 대신 기본 지구본 PNG만 돌려주는 경우가 확인됐다. 같은 서비스의 `faviconV2`에 `https://` origin을 넘기면 실제 아이콘을 받을 수 있으므로 `lib/external-favicon-url.js`를 V2 URL 생성으로 바꿨다. Google CDN 캐시 특성상 파비콘 변경 반영은 여전히 지연될 수 있어, 확실한 표시가 필요하면 관리자 썸네일 URL 등록을 유지한다.
## 2026-06-17 v1.5.109 — 추천 사이트는 태그 아래 탐색 영역에 둔다
추천 사이트 목록은 시간이 지날수록 늘어나는 탐색 보조 정보이므로 오른쪽 사이드바에 두면 광고 슬롯 높이와 게시물 상세 목차 흐름을 예측하기 어려워진다. 추천 사이트를 왼쪽 사이드바의 Categories 아래로 옮겨 태그와 같은 탐색 맥락에 묶고, 오른쪽 사이드바는 일반 화면 광고와 게시물 상세 TOC·광고에 집중하도록 분리한다.
## 2026-06-17 v1.5.107 — 업로드 파일명은 Unicode 문자 범위로 보존한다
이미 한글 파일명을 보존하고 있으므로 일본어 파일명만 `image.jpg`로 떨어지는 것은 사용자 기대와 맞지 않는다. 파일명 정리는 영문·한글 고정 목록이 아니라 Unicode 문자·숫자 범위를 보존하고, 경로 구분자와 특수문자만 하이픈으로 치환하는 방식으로 확장한다. 이렇게 하면 일본어와 한자를 포함한 파일명도 유지하면서 경로 안전성은 기존처럼 확보할 수 있다.
## 2026-06-17 v1.5.106 — 게시물 HTML 블록은 iframe만 허용한다
외부 자료 공유 코드는 작성자에게는 HTML 블록처럼 보이지만, 게시물 본문에서 임의 HTML 전체를 실행하면 보안과 레이아웃 영향 범위가 커진다. 그래서 기존 임베드 저장 타입은 호환을 위해 유지하되 UI 라벨은 HTML로 바꾸고, 실제 HTML 처리는 iframe 코드의 안전한 속성만 추출해 sandbox iframe으로 렌더링하는 범위로 제한한다.
## 2026-06-16 v1.5.105 — 텍스트 색상은 제한된 인라인 토큰으로 저장한다
게시물 본문에서 중요한 단어나 덜 중요한 단어를 색으로 구분하려면 작성 UI에서는 오른쪽 패널 선택 흐름이 자연스럽지만, 저장 형식은 임의 HTML style보다 검증 가능한 마크다운 확장 토큰이 안전하다. 따라서 선택 텍스트 색상은 `::red[텍스트]`처럼 제한된 인라인 토큰으로 저장하고, 렌더러는 `gray`, `blue`, `green`, `yellow`, `red`, `purple` 프리셋만 해석한다. 이렇게 하면 공개 화면 다크 모드 색상 보정과 라이브 편집 직렬화를 한 곳에서 관리할 수 있다.
## 2026-06-12 v1.5.98 — 게시물 TOC는 내부 스크롤보다 자연스러운 흐름을 우선한다
오른쪽 사이드바의 TOC에 별도 최대 높이를 두면 광고 영역은 항상 보장되지만, 긴 목차가 작은 박스 안에서 따로 스크롤되어 문서 탐색과 광고 배치가 분리되어 보인다. 게시물 상세에서는 Tailwind 문서처럼 TOC 전체가 먼저 펼쳐지고 광고가 그 아래에 이어지는 흐름이 더 자연스러우므로, TOC 자체 높이 제한을 제거하고 활성 항목 보정은 오른쪽 사이드바 전체 스크롤 기준으로 처리한다.
## 2026-06-12 v1.5.97 — 게시물 상세 사이드바는 TOC와 광고를 오른쪽에 함께 둔다
게시물 상세에서는 오른쪽 사이드의 블로그 소개와 Follow보다 본문 탐색과 광고가 우선된다. TOC를 최상단으로 올리고 높이를 제한해 아래 광고 영역을 확보하면 목차와 광고가 같은 문맥 안에서 보이며, 왼쪽 사이드바는 기존 네비게이션 역할에 집중할 수 있다. TOC에는 세로 기준선을 추가해 제목 레벨 들여쓰기와 활성 위치가 더 명확하게 보이도록 한다.
## 2026-06-12 v1.5.96 — 공개 화면 세션 확인은 선택적 조회를 사용한다
공개 화면의 헤더, 댓글, 게시물 상세 편집 버튼은 로그인 여부를 확인해야 하지만 비로그인 방문도 정상 사용 흐름이다. 기존처럼 `/api/auth/me`와 `/admin/api/auth/me`가 401을 반환하면 UI는 정상이어도 브라우저 콘솔에 오류가 반복 표시된다. 따라서 `optional=1` 쿼리에서는 비로그인 상태를 `null`로 반환하고, 관리자 라우트 가드와 보호 API가 사용하는 기본 호출은 401을 유지한다.
## 2026-06-09 v1.5.93 — 태그 제한은 사이트 설정으로 관리하고 표는 표준 마크다운부터 지원한다
게시물 태그는 목록 UI와 공개 카드에서 스캔성을 좌우하므로 기본 최대 5개로 제한한다. 운영자가 블로그 성격에 맞게 조절할 수 있도록 `site_settings.post_tag_limit`으로 저장하되, 과도한 태그 입력을 막기 위해 허용 범위는 1~10개로 제한하고 글쓰기 UI와 저장 API 양쪽에서 검증한다. 표 기능은 셀 단위 라이브 편집까지 한 번에 확장하면 에디터 안정성에 영향이 크므로, 먼저 표준 마크다운 표 삽입과 렌더링을 지원해 콘텐츠 작성에 필요한 기본 기능을 제공한다.
## 2026-06-09 v1.5.91 — 게시물 태그 표시 순서는 저장 순서로 고정한다
게시물에 여러 태그를 설정했을 때 공개 목록은 작성자가 입력한 첫 번째 태그를 기준으로 보여 주어야 한다. 기존 `post_tags` 연결 테이블에는 게시물 안에서의 순서가 없어 PostgreSQL `array_agg` 결과가 안정적이지 않을 수 있으므로, `post_tags.sort_order`를 추가해 저장 시 입력 순서를 기록하고 조회 시 이 값을 기준으로 정렬한다. 관리자 게시물 목록은 운영 확인용 화면이므로 대표 태그 하나가 아니라 적용된 태그 전체를 보여 준다.
## 2026-06-08 v1.5.82 — sitemap은 파일이 아니라 공개 DB 기준 동적 응답으로 제공한다
게시물을 발행할 때마다 정적 sitemap 파일을 다시 쓰는 방식은 운영 볼륨과 배포 산출물의 경계가 흐려지고, 예약 발행·검색엔진 노출 제외 상태와 어긋나기 쉽다. sitemap은 요청 시점의 공개 DB 상태에서 만들고, `noindex` 글과 멤버십·비공개·초안·예약 대기 글은 제외한다. robots.txt는 sitemap 위치를 명시해 검색엔진이 RSS나 내부 링크만 추적하지 않아도 공개 URL 목록을 찾을 수 있게 한다.
## 2026-06-08 v1.5.80 — 카드 썸네일 생성은 대표 이미지 저장 시점으로 제한한다
본문에 첨부한 모든 이미지는 사용자가 콘텐츠 일부로 올린 원본 자산이며, 목록 카드에 쓰이지 않을 수 있다. 업로드 시점에 모든 본문 이미지의 카드 썸네일을 만들면 미디어 관리 화면에 파생 파일이 불필요하게 늘어나고, 어떤 썸네일이 실제로 필요한지 판단하기 어려워진다. 따라서 업로드 API는 원본만 저장하고, 게시물 대표 이미지로 저장되는 이미지에 한해 카드 썸네일을 생성한다. 썸네일 파일이 지워졌거나 누락된 대표 이미지는 관리자 미디어 상세에서 다시 생성할 수 있게 두고, 상세 본문 상단 대표 이미지는 글별 옵션으로 기본 숨김 처리한다.
## 2026-06-08 v1.5.79 — 카드 썸네일은 별도 탭과 원본 연결 사용처로 관리한다
게시물 카드 썸네일은 사용자가 직접 본문에 삽입하는 원본 미디어가 아니라 목록 성능을 위해 자동 생성되는 파생 파일이다. 일반 미디어 라이브러리에 원본과 썸네일을 함께 노출하면 같은 이미지가 두 개씩 보이고, 미사용 정리 과정에서 실제 목록에 쓰는 썸네일을 삭제할 위험이 있다. 따라서 카드 썸네일을 별도 탭으로 분리하고, 사용 여부는 썸네일 URL 자체가 아니라 원본 대표 이미지 사용처에 연결해 판단한다. 원본이 대표 이미지로 쓰이지만 썸네일이 없으면 목록에서 원본을 불러오는 fallback 상태로 표시해 백필 필요 여부를 구분한다.
## 2026-06-08 v1.5.78 — 공개 목록 이미지는 원본 대신 카드 썸네일을 우선 사용한다
대표 이미지는 상세 화면, OG 이미지, RSS 같은 원본 품질이 필요한 경로에서도 쓰인다. 하지만 메인·목록·태그 카드에서는 작은 썸네일만 필요하므로 사이트 접속만으로 큰 원본 이미지를 내려받는 비용이 크다. 업로드 시점에 640×360 WebP 카드 썸네일을 함께 만들고, 공개 목록 응답에는 파일이 실제로 존재할 때만 `featuredImageThumbnail`을 추가해 기존 이미지와 외부 URL의 fallback을 유지한다. 기존 업로드 파일은 운영 볼륨에 저장되므로 저장소에 포함하지 않고 백필 명령으로 생성한다.
## 2026-06-08 v1.5.77 — Latest 목록은 기존 썸네일 레이아웃을 유지하며 메타 잘림을 줄인다
메인 Latest 목록은 빠르게 훑는 영역이므로 설명이 길어질 때 메타 정보가 잘려 보이면 스캔성이 떨어진다. 썸네일 레이아웃을 바꾸면 기존 compact 보기의 균형이 깨질 수 있으므로 썸네일 구조는 유지하고, 요약 영역이 메타 행을 밀어내지 않도록 최소 범위에서 조정한다.
## 2026-06-05 v1.5.75 — 작성 통계는 광고 기준과 같은 문자 기준을 보여준다
게시물 인아티클 광고가 본문 길이를 기준으로 노출되므로 작성 화면에서도 길이 감각을 바로 확인할 수 있어야 한다. 작성 흐름을 방해하지 않도록 본문 위에 떠 있는 표시 대신 오른쪽 설정 패널 하단에 작은 통계 영역을 두고, 문자 수는 광고 기준과 같은 공백 제외 값을 우선 표시한다. 단어 수와 읽기 시간, 블록·이미지 수는 글의 분량과 구성을 빠르게 파악하는 보조 정보로 함께 둔다.
## 2026-06-05 v1.5.74 — 게시물 상세 광고는 목차와 분리한다
게시물 상세 오른쪽 사이드바는 본문 목차가 핵심 기능이므로 공통 사이드 광고와 함께 두면 폭과 클릭 맥락이 어색해질 수 있다. 따라서 공통 오른쪽 사이드 광고는 게시물 상세가 아닌 화면에만 유지하고, 게시물 상세에는 별도 왼쪽 사이드 광고 슬롯을 둔다. 인아티클 광고는 상단·하단 광고와 함께 쓰이므로 짧은 글에서는 생략하고, 충분히 긴 글에서만 본문 흐름을 해치지 않도록 1회 또는 최대 2회로 제한한다.
## 2026-06-05 v1.5.73 — 게시물 인아티클 광고는 충분히 긴 본문 40% 지점에 1회 삽입
게시물 상세에는 이미 본문 상단·하단 광고 슬롯이 있으므로 인아티클 광고는 너무 자주 노출하지 않는다. 공개 렌더러가 편집 모드가 아닐 때만 광고 코드를 받고, 본문 블록이 충분히 길며 일반 문단 후보가 여유 있게 있을 때 전체 블록 40% 근처 문단 뒤에 한 번 삽입한다. 짧은 글이나 문단 후보가 부족한 글에서는 광고를 생략해 본문 흐름과 상하단 광고 간격을 유지한다.
## 2026-06-05 v1.5.72 — 메인 인피드 광고는 클라이언트에서 한 번 무작위 배치
메인 Latest 목록 사이 광고는 매번 같은 위치보다 자연스럽게 보이도록 무작위 삽입을 허용한다. 다만 SSR 단계에서 무작위 값을 만들면 서버 HTML과 클라이언트 hydration 결과가 달라질 수 있으므로, 브라우저 마운트 이후 게시물 사이 한 지점을 한 번 산출해 삽입한다. 광고는 첫 항목 앞이나 마지막 항목 뒤가 아니라 게시물 사이에만 들어가도록 제한한다.
## 2026-06-05 v1.5.71 — 화면 위치 광고 슬롯은 사이트 코드와 분리 관리
사이트 검증·공통 스크립트용 사이트 코드 카드와, 실제 화면 위치에 삽입되는 광고 단위를 분리했다. 이유는 ads.txt/head/footer는 전역 설정이고, 메인 피드·사이드바·게시물 본문 상단·하단 광고는 위치별 표시 여부와 레이아웃 책임이 다르기 때문이다. 광고 HTML은 관리자 신뢰 콘텐츠로 저장하되, 빈 값은 DOM을 만들지 않고 클라이언트에서만 삽입해 AdSense 스크립트 중복 실행 가능성을 줄인다.
## 2026-06-05 v1.5.70 — 라이브 멀티라인 Enter는 DOM 조각 삽입 대신 텍스트 값을 갱신한다
콜아웃·인용 본문은 `white-space: pre-wrap`인 plain text contenteditable로 관리한다. Range에 직접 텍스트 노드를 삽입하면 줄바꿈 직후 브라우저 IME 조합 위치가 불안정해져 한글 첫 글자가 자모로 분리될 수 있다. 멀티라인 Enter는 전체 텍스트 값을 기준으로 선택 범위를 `\n`으로 교체하고 커서를 텍스트 오프셋으로 다시 배치한다. 또한 Selection Bridge의 Range 교차 판정을 바로잡고, 콜아웃·인용 전체 선택 삭제는 블록 자체가 아니라 빈 본문 줄을 남기도록 한다.
## 2026-06-05 v1.5.69 — 멀티라인 라이브 편집은 끝 줄바꿈을 보존한다
인용·콜아웃 본문은 하나의 contenteditable 안에서 여러 원본 줄을 대표한다. 이 값 읽기에서 끝 줄바꿈을 잘라내면 마지막 줄에서 Enter를 눌러도 모델에는 새 줄이 남지 않고, 삭제·병합 같은 구조 변경 뒤에는 포커스 중인 오래된 DOM이 blur 때 다시 저장될 수 있다. 멀티라인 plain text 편집은 끝 줄바꿈을 보존해 읽고, 줄 삭제·병합 전에는 stale blur 커밋을 잠시 차단해 부모 모델 갱신이 우선되게 한다.
## 2026-06-05 v1.5.68 — Shift+위/아래 선택은 커서 위치 기준으로 확장한다
라이브 편집은 블록별 contenteditable 구조라 단일 줄 문단 안에서 브라우저 기본 Shift+위/아래가 다음 블록으로 자연스럽게 이어지지 않는다. 블록 끝에서만 확장하는 방식은 textarea의 줄 선택 경험과 달라 실제 글쓰기에서 실패처럼 느껴지므로, 단일 줄 블록에서는 커서 위치와 상관없이 Selection Bridge가 인접 블록의 같은 열로 선택을 확장하도록 바꾼다. 연속 확장 시에는 `sourceLine`이 아니라 Selection의 focus 노드가 속한 편집 요소를 현재 블록으로 사용한다.
## 2026-06-05 v1.5.67 — 라이브 교차 선택 삭제는 DOM이 아니라 마크다운을 갱신한다
교차 블록 선택은 DOM에는 보이지만 각 contenteditable의 키 처리만으로는 삭제가 반영되지 않는다. `lib/markdown-live-selection.js`가 선택 범위를 원본 줄·편집 본문 오프셋으로 변환하고, `ContentMarkdownRenderer`가 `content-replace`로 상위 에디터 본문을 한 번에 갱신한다. Shift+방향키 확장은 줄 범위 탐색과 선택 포커스 경계 판별을 보강하고, Shift 조합 시 블록 이동 단축키와 충돌하지 않게 분리한다.
## 2026-06-05 v1.5.66 — 라이브 선택은 블록 편집 구조를 유지한 채 브리지로 이어준다
라이브 모드를 단일 contenteditable 문서로 바꾸면 Enter·IME·블록 옵션·카드형 블록 편집과 충돌한다. 대신 각 블록 편집기는 유지하고 `ContentMarkdownRenderer`가 Selection API로 Shift 범위 확장과 단계적 전체 선택을 중개한다.
## 2026-06-05 v1.5.65 — IME 확정 Enter의 keyup 경로도 처리한다
일부 브라우저·한글 IME 조합에서는 마지막 글자 확정에 Enter를 쓰면 `keydown`이 contenteditable까지 전달되지 않고 `compositionend` 뒤 `keyup`만 남는다. 이 경우 기존 pending 방식만으로는 줄바꿈 동작을 예약할 수 없어 Enter를 한 번 더 눌러야 했다. 조합 종료 직후 짧은 시간 안에 들어온 Enter `keyup`도 같은 물리 키 입력으로 보고, 현재 Enter 모드에 맞는 블록 동작을 한 번만 실행한다.
## 2026-06-05 v1.5.64 — 텍스트 선택은 블록 이동보다 우선한다
라이브 편집은 블록 단위 contenteditable이 여러 개이므로 문서 전체를 한 번에 드래그 선택할 수는 없다. 다만 각 편집 영역 안에서는 Shift+방향키·전체 선택이 반드시 동작해야 한다. 방향키 블록 이동 단축키가 Shift 조합까지 가로채면 본문 편집이 망가지므로, 선택 제스처일 때는 커스텀 키 처리를 건너뛴다.
## 2026-06-05 v1.5.63 — 라이브 편집 IME Enter 처리는 공통 컴포넌트에서 통일한다
한국어 입력 중 Enter는 브라우저와 IME에 따라 글자 조합 확정 이벤트와 키 입력 이벤트 순서가 다르게 들어온다. 이를 블록별 컴포넌트에서 따로 처리하면 문단·목록·토글 제목·멀티라인 본문 사이의 동작이 다시 달라질 수 있다. 라이브 편집의 모든 텍스트 블록이 `ContentMarkdownEditableInline`을 거치므로, 조합 중 Enter를 공통 pending 동작으로 저장하고 `compositionend` 직후 각 Enter 모드의 실제 동작을 실행하도록 통일한다.
## 2026-05-26 v1.5.62 — Enter 분리 직후 포커스 중 DOM을 modelValue에 맞춘다
문단 Enter 분리는 마크다운을 먼저 갱신하고 Vue가 `modelValue`를 줄인 뒤에도, 포커스 중인 contenteditable은 병합 직전의 긴 문자열을 DOM에 남긴다. 이 상태에서 blur가 나가면 통째 줄이 다시 커밋되어 아래 줄이 복제된 것처럼 보인다. 분리·병합 직후에는 `modelValue`와 DOM을 강제 동기화하고, 구조 변경 직후의 stale blur 커밋은 차단한다.
## 2026-05-26 v1.5.61 — 멀티라인 포커스 시 빈 줄은 편집 영역 전체를 비우지 않는다
콜아웃·코드·토글 본문은 하나의 contenteditable이 `data-source-line`~`data-source-line-end`로 여러 원본 줄을 대표한다. 소스·라이브 전환 후 `focusEditableAtLine`은 대상 원본 줄이 비어 있으면 단일 줄 편집기처럼 DOM을 비우는데, 범위의 첫 줄이 비어 있을 때는 멀티라인 편집기 전체가 지워져 blur 커밋으로 마크다운까지 유실된다. 빈 줄 초기화는 단일 줄 편집기에만 적용하고, 줄 범위가 2줄 이상인 편집기는 해당 원본 줄 위치로 커서만 둔다.
## 2026-06-04 v1.5.54 — 콜아웃 제목 옵션과 기본 아이콘 미사용
콜아웃은 본문 시작 전 시각 신호를 독립적으로 보여주는 편이 여러 줄 본문에서 정렬이 안정적이다. 따라서 라이브·공개 렌더링을 아이콘·제목 헤더와 본문 영역으로 분리하고, 제목은 기존 콜아웃 선언부에 `title` 옵션으로 저장한다. 새 콜아웃은 본문 작성 부담을 줄이기 위해 기본 아이콘 표시를 끄고, 필요할 때 오른쪽 블록 설정 패널에서 켜도록 한다.
## 2026-06-04 v1.5.47 — RSS 썸네일은 피드 XML에 명시한다
게시물 상세 페이지에는 대표 이미지 기반 `og:image`가 있지만, RSS 리더가 게시물 링크를 다시 크롤링해 OG 이미지를 읽는지는 리더마다 다르다. 피드 구독 화면에서 썸네일 노출을 더 안정적으로 만들기 위해 RSS item에 Media RSS 확장인 `media:thumbnail`과 `media:content`를 직접 포함한다. 저장된 이미지 경로가 상대 경로일 수 있으므로 피드 생성 시 사이트 URL 기준 절대 URL로 변환한다.
## 2026-06-04 v1.5.46 — 콜아웃과 인용 팔레트는 하나로 맞춘다
콜아웃과 인용은 모두 작성자가 본문 안에서 특정 문맥을 강조하는 블록이므로 배경색 선택지가 서로 다르면 오른쪽 설정 패널의 의미가 흐려진다. 분홍은 빨강과 역할이 겹치고 실제 팔레트 변경 후에도 남아 있던 선택지라 제거하고, 두 블록 모두 같은 6색 팔레트를 사용한다. 라이브 편집의 방향키 이동은 옵션 선언 줄처럼 화면에 직접 편집 영역이 없는 줄을 건너뛰어야 하므로, 탐색 대상은 실제 contenteditable 줄과 카드형 블록으로 제한한다. 작은 화면의 게시물 설정 패널은 문서를 밀어내지 않는 고정 오버레이로 두어 본문 폭을 압축하지 않게 한다.
## 2026-06-04 v1.5.45 — 라이브 작성 블록 옵션도 오른쪽 패널로 통일한다
소스 모드에서는 커서가 옵션을 가진 블록 안에 들어가는 것만으로 오른쪽 설정 패널이 열리는데, 라이브 모드 콜아웃은 별도 모달을 열어 같은 기능을 다른 방식으로 제공하고 있었다. 작성자가 모드에 따라 다른 조작을 기억하지 않도록 콜아웃과 인용 블록 모두 라이브 영역에 포커스가 들어오면 오른쪽 패널을 쓰도록 통일한다. 인용 색상은 새 팔레트 기준으로 회색을 기본값으로 두고, 빨강과 역할이 겹치는 분홍은 선택지에서 제거한다.
## 2026-06-02 v1.5.43 — RSS는 공개 발행글 피드로만 제공한다
RSS 리더는 공개 화면을 구독하는 용도이므로 멤버십, 비공개, 초안, 아직 발행 시간이 오지 않은 예약 글은 피드에 포함하지 않는다. 호환성을 위해 대표 경로 `/rss.xml`과 흔히 쓰는 `/feed.xml`, `/rss` 별칭을 함께 제공하되, 관리자 SNS 프리셋은 표준적인 `/rss.xml`을 기본 주소로 둔다.
## 2026-06-02 v1.5.41 — SNS 편집은 아이콘과 주소만으로 충분하다
FOLLOW 영역은 아이콘 버튼 목록이므로 운영자가 직접 보는 편집 화면에서도 핵심 입력은 아이콘과 주소다. 이름은 접근성 라벨로 내부에서 프리셋명을 쓰면 충분하고, 별도 입력칸은 레이아웃을 복잡하게 만든다. 저장값은 JSONB 배열이어야 하므로 기존 문자열 저장값은 마이그레이션으로 복구하고, 저장 쿼리도 명시적으로 JSONB 배열로 캐스팅한다.
## 2026-06-02 v1.5.40 — SNS 아이콘은 프리셋과 직접 SVG를 함께 둔다
SNS 채널은 서비스 유행에 따라 계속 바뀌므로, Facebook·LinkedIn 같은 고정 필드만 두면 운영자가 쓰지 않는 항목과 새로 필요한 항목 사이에서 계속 코드 수정이 필요해진다. 기본 아이콘은 프리셋으로 빠르게 선택하게 두되, 프리셋에 없는 서비스는 관리자가 SVG 아이콘을 직접 붙여 넣어 같은 목록 구조로 저장하도록 했다. URL은 운영자가 `https://`를 생략해도 정상 링크로 보정해 저장 실패처럼 보이는 혼란을 줄인다.
## 2026-06-02 v1.5.39 — SNS 링크는 고정 필드보다 목록형이 맞다
오른쪽 사이드바 FOLLOW 영역은 사이트마다 사용하는 채널이 크게 다르다. Facebook이나 LinkedIn처럼 쓰지 않는 채널을 고정 필드와 기본 아이콘으로 계속 노출하면 실제 운영 화면과 맞지 않으므로, 사이트 설정에는 아이콘 프리셋과 주소의 목록을 저장한다. 주소가 있는 항목만 공개 화면에 렌더링해 FOLLOW 영역 자체가 운영자가 설정한 채널만 반영하도록 했다.
## 2026-06-02 v1.5.38 — 어나운스 바는 운영자가 브랜드 톤에 맞춘다
어나운스 바는 모든 공개 화면 상단에 노출되는 안내 요소라 프리셋 몇 개만으로는 사이트별 브랜드 톤을 맞추기 어렵다. 브랜드 컬러처럼 hex 색상 입력을 허용하되, 텍스트 대비색은 배경 밝기에 따라 자동 계산해 가독성을 유지한다. 문구 정렬은 중앙과 왼쪽 두 가지로 제한해 닫기 버튼이 오른쪽 끝에 남는 기존 구조를 유지하면서도 공지 성격에 맞게 배치할 수 있게 했다.
## 2026-06-02 v1.5.37 — 블록 옵션은 본문 옆 설정 패널로 모은다
콜아웃, 코드 블록, 토글은 본문 안에서 직접 편집할 수 있지만 옵션 조작 위치가 블록마다 다르면 작성 흐름이 끊긴다. 인용문 배경색처럼 커서가 놓인 블록을 오른쪽 패널에서 조정하도록 통일하면, 본문 텍스트와 구조 옵션을 분리해 다룰 수 있다. 옵션은 별도 메타 저장소를 만들지 않고 각 fenced 블록의 선언 줄에 반영해 Markdown 백업·Import 흐름과도 계속 호환되게 둔다.
## 2026-06-02 v1.5.36 — 포인트 컬러는 테마 파일이 아니라 운영 설정이다
사용자 화면의 활성 네비게이션, TOC, 댓글 버튼은 사이트의 브랜드 인상을 만드는 공통 강조 요소다. 코드에 고정된 오렌지색을 각 컴포넌트에서 따로 바꾸면 운영자가 브랜드를 바꾸기 어렵고 누락도 생기므로, 사이트 설정의 `brand_color`를 공개 앱 루트의 `--site-accent` CSS 변수로 주입한다. 기존 색상은 기본값으로 유지해 저장값이 없거나 마이그레이션 전 환경에서도 같은 화면을 보여 준다.
## 2026-06-02 v1.5.35 — 유입 분석은 축약 집계로 시작한다
관리자 대시보드는 방문자가 어디서 들어왔는지, 어떤 기기로 보는지, 검색 키워드가 있는지 정도를 빠르게 파악할 수 있어야 한다. 다만 원문 referrer나 IP를 오래 저장하면 운영 부담과 개인정보 리스크가 커지므로, 페이지뷰 요청 시 서버가 즉시 검색·SNS·직접·기타, 디바이스·OS, 키워드로 축약해 일별 집계만 남긴다. 검색 키워드는 검색엔진이 referrer query를 전달한 경우에만 수집되며, 숨겨진 키워드는 소급하거나 추정하지 않는다.
## 2026-06-02 v1.5.34 — 사이트 검증 코드는 설정으로 관리한다
AdSense의 `ads.txt`나 헤더 검증 스크립트는 테마 파일을 직접 고치기보다 운영 설정에서 바꾸는 편이 안전하다. 그래서 사이트 설정에 ads.txt, 헤더 코드, 푸터 코드를 별도 카드로 두고, 공개 HTML 응답과 루트 `/ads.txt`에서만 반영한다. 관리자 화면과 API 응답에는 삽입하지 않아 관리 도구가 외부 광고·검증 스크립트에 영향을 받지 않게 했다. gethomepage 연동은 아직 표시 항목이 확정되지 않았으므로 기존 통계 집계에서 바로 만들 수 있는 오늘 방문자·페이지뷰·현재 접속자·평균 체류시간을 반환하는 커스텀 API 틀로 시작한다.
## 2026-06-02 v1.5.33 — 완료 상태는 조용히 둔다
최근 내보내기 작업에서 준비 완료는 이미 다운로드 가능한 파일 목록으로 충분히 드러난다. 완료 배지를 계속 표시하면 정리된 다운로드 카드 안에서 불필요한 시각 요소가 되므로, 대기·생성·실패처럼 주의가 필요한 상태에서만 배지를 보여 준다.
## 2026-06-02 v1.5.32 — 완료된 백업은 선택 다운로드만 남긴다
내보내기 작업이 준비 완료된 뒤에는 진행도와 요청 조건보다 어떤 ZIP을 받을지가 더 중요하다. 한 파일만 있을 때도 일괄 다운로드와 개별 다운로드 버튼이 동시에 보이면 행동이 중복되어 보이므로, 완료 작업은 만료일만 남기고 파일 체크 선택과 선택 파일 다운로드로 정리한다. 진행도는 대기·생성 중 작업의 상태 확인용으로만 표시한다.
## 2026-06-02 v1.5.31 — 백업 요청과 다운로드 결과는 분리해서 보여 준다
내보내기 설정 카드는 새 백업 작업을 만드는 입력 영역이고, 최근 작업 목록은 이미 생성되었거나 생성 중인 결과 영역이다. 두 흐름을 같은 카드 안에 두면 접기·펼치기 상태와 다운로드 상태가 한 덩어리처럼 보이므로, 최근 작업은 작업이 있을 때만 별도 카드로 표시해 요청 설정과 결과 확인을 분리한다.
## 2026-06-02 v1.5.30 — 내보내기와 가져오기는 서로 다른 작업이다
게시물 백업 생성과 백업 ZIP 복원은 사용 빈도와 위험도가 다르다. 특히 가져오기는 거의 쓰지 않지만 한 번 실행하면 게시물이 새로 생성되므로, 파일 선택 즉시 실행하지 않고 `적용` 버튼을 명시적으로 누르게 했다. 설정 화면의 기본 철학도 현재 상태를 먼저 보여 주고 조작은 버튼 뒤에 숨기는 쪽이므로, 내보내기와 가져오기를 독립 카드로 나누고 각 상세 입력은 아코디언처럼 펼쳐지게 정리한다.
## 2026-06-02 v1.5.29 — 라이트·다크 커버는 각각 확인 가능해야 한다
홈 커버 이미지는 라이트·다크 테마에서 서로 다른 시각 결과를 만들 수 있다. 하나의 `HomeHero` 미리보기만 두면 현재 OS나 테마 상태에 따라 한쪽 이미지를 확인하기 어렵기 때문에, 관리자 설정에서는 라이트모드와 다크모드를 상하 개별 프리뷰로 항상 보여 준다. 아직 실제 동작이 없는 타임존 설정은 노출하지 않고, 로고·URL·저작권은 포괄적인 `사이트 정보`로 이름을 정리한다.
## 2026-06-02 v1.5.28 — Import는 부분 누락을 경고로 남긴다
Export ZIP을 Obsidian에서 열고 편집한 뒤 다시 Import할 수 있어야 하므로, 태그 frontmatter는 inline 배열뿐 아니라 Obsidian이 흔히 쓰는 블록 배열도 읽는다. 또한 일부 이미지나 파일이 ZIP에서 누락된 경우 전체 Import를 막으면 나머지 게시물 복구까지 실패하므로, 게시물 생성은 계속 진행하고 누락 자산은 관리자 화면 경고로 확인하게 한다.
## 2026-06-02 v1.5.27 — Import 1차는 덮어쓰기보다 안전한 추가 가져오기
Export ZIP은 백업 복구와 다른 환경으로의 이관에 모두 쓰일 수 있다. 같은 슬러그가 이미 있는 운영 DB에서 Import가 기존 글을 바로 덮어쓰면 복구보다 손상 위험이 커지므로, 1차 Import는 모든 게시물을 새 글로 추가하고 슬러그 충돌 시 `-2`, `-3` 접미사를 붙이는 정책으로 둔다. ZIP 내부 자산은 원래 URL을 믿지 않고 새 업로드 저장소로 다시 복사해 본문 경로를 새 `/uploads/...` URL로 재매핑한다.
## 2026-06-02 v1.5.26 — Import/Export 설정은 필요할 때만 펼친다
게시물 Import/Export는 운영자가 매일 조작하는 기본 설정이 아니라 필요할 때 요청하거나 다운로드하는 백업 도구다. 상세 범위와 분할 설정이 항상 카드 상단을 크게 차지하면 다른 설정을 훑는 흐름이 무거워지므로, 기본 화면은 `Export 요청`과 `Import 하기` 진입 버튼, 그리고 요청된 작업의 다운로드 상태 중심으로 줄였다. Export 조건 입력은 버튼을 눌렀을 때만 펼치고, 아직 실제 가져오기 API가 연결되지 않은 Import는 접힌 안내 패널로만 둔다.
## 2026-06-01 v1.5.25 — Export 분할은 용량 기준이 우선이다
기존 100개 단위 분할은 대략적인 안내값으로는 충분하지만, 게시물마다 첨부 자산 크기가 다르면 실제 ZIP 용량을 예측하기 어렵다. Export 계획은 게시물 본문 크기와 내부 업로드 자산 크기를 추산해 목표 ZIP 용량을 넘기기 전에 새 파일로 나누도록 바꿨다. ZIP당 최대 게시물 수는 용량 기준을 대신하는 규칙이 아니라 너무 많은 작은 글이 한 파일에 몰리는 상황을 막는 안전 상한으로만 남겼다. 실패 원인은 재시도 여부를 판단하는 운영 정보이므로 작업 카드에서 확인할 수 있게 별도 상세 로그로 저장한다.
## 2026-06-01 v1.5.24 — Export는 완료 후에도 관리 가능한 작업이어야 한다
대용량 Export는 파일 생성이 끝난 뒤에도 운영자가 여러 ZIP을 안정적으로 내려받고, 실패한 지점부터 다시 이어가고, 오래된 산출물을 치울 수 있어야 한다. 브라우저 일괄 다운로드는 준비 완료 파일을 순차로 요청하는 방식으로 두어 중간에 막혀도 개별 파일 버튼으로 이어받을 수 있게 했다. 실패 작업 재시도는 이미 생성된 ZIP을 유지하고 나머지 분할 파일만 다시 대기 상태로 되돌린다. 만료된 완료·실패 작업은 목록 조회나 새 요청 시 자동 삭제해 100일 보존 정책이 실제 저장 공간 정리로 이어지게 했다.
## 2026-06-01 v1.5.23 — Export는 필요한 기간만 꺼낼 수 있어야 한다
전체 백업은 최초 백업이나 큰 이관에는 필요하지만, 운영 중 반복 백업에서는 같은 게시물과 자산을 계속 다시 묶어 시간과 저장 공간을 낭비한다. Export 요청에 전체·특정년·특정월·직접 지정 날짜 범위를 추가해 필요한 기간만 백업할 수 있게 했다. 게시물 기준일은 발행일이 있으면 `published_at`, 없으면 `created_at`으로 판단한다. 생성 완료된 백업은 사용자가 바로 삭제할 수 있어야 서버 업로드 볼륨을 관리하기 쉽기 때문에, 완료·실패 작업 삭제 API도 함께 둔다.
## 2026-06-01 v1.5.22 — Export는 실제 파일 생성 단계로 연결
Export 작업이 대기열과 진행도 표시까지만 존재하면 관리자는 같은 버튼을 반복해서 누르기 쉽고, 실제 백업 파일도 받을 수 없다. 이번 단계에서는 요청 직후 서버 프로세스가 대기 작업을 백그라운드로 실행해 분할 ZIP을 만들고, 준비 완료된 파일만 다운로드할 수 있게 했다. 내부 `/uploads` 자산은 ZIP 내부의 `images/` 또는 `files/` 폴더로 복사하고 Markdown 참조는 상대 경로로 바꿔, 원본 서버 URL이 사라져도 Obsidian에서 백업을 열 수 있는 형태를 우선했다. 대기 중·생성 중 작업이 있으면 새 Export 요청 버튼은 잠가 중복 작업 생성을 막는다.
## 2026-06-01 v1.5.21 — Export 작업은 숫자로 진행 여부를 확인
Export 작업이 대기열에만 보이면 관리자는 실제로 서버가 일하고 있는지 알 수 없다. 다운로드가 아직 연결되지 않았더라도 작업 레코드에는 전체 게시물 수 대비 처리된 게시물 수, 현재 분할 파일 순번, 진행 메시지, 시작 시각을 저장할 수 있어야 한다. 설정 화면은 진행 중 작업이 있을 때 주기적으로 목록을 새로고침하고 `1201 / 30002` 같은 숫자와 진행률 바를 보여 주도록 했다. 이 기반은 v1.5.22의 zip 생성 워커에서 실제 값 갱신에 사용한다.
## 2026-06-01 v1.5.20 — Export 구현은 작업 등록부터 단계적으로 연결
대용량 Export는 한 번에 UI, zip 생성, 이메일, 다운로드까지 붙이면 실패 지점 파악이 어렵다. 먼저 `post_export_jobs`와 `post_export_files`로 요청과 분할 파일 계획을 영속화하고, 관리자 설정 화면에서는 이 계획을 확인할 수 있게 했다. 실제 zip 생성 워커와 다운로드 API는 같은 작업/파일 레코드를 기준으로 이어 붙이면 되므로 이후 단계에서 재시도·만료·이메일 알림을 독립적으로 구현할 수 있다.
## 2026-06-01 v1.5.19 — 대용량 Export는 요청과 다운로드를 분리
게시물이 수만 개까지 늘어나면 관리자가 Export 버튼을 누르는 순간 모든 게시물과 자산을 하나의 zip으로 만드는 방식은 요청 타임아웃, 메모리 사용량, 브라우저 다운로드 실패 가능성이 크다. Export는 요청 즉시 파일을 내려주는 기능이 아니라 백그라운드 작업으로 분리한다. 서버는 게시물 개수나 산출 용량 기준으로 여러 zip을 만들고, 준비가 끝나면 이메일로 알린다. 관리자 화면은 각 분할 파일을 독립적으로 다운로드할 수 있게 하며, 일괄 다운로드는 브라우저에서 순차 실행해 중간 실패 시 해당 범위부터 다시 받을 수 있게 한다. 산출물은 백업 생성물이라 서버 용량을 계속 차지하므로 최대 100일 후 만료·삭제한다.
## 2026-06-01 v1.5.18 — 게시물 백업은 URL이 아닌 로컬 자산 번들
게시물 Export는 다른 도구로 읽는 이동성뿐 아니라, 원본 서버나 `/uploads` URL이 사라져도 복원 가능한 백업이어야 한다. 따라서 Markdown 본문이 기존 URL을 그대로 참조하는 방식은 백업 요구에 맞지 않다. 게시물마다 폴더를 만들고 `제목.md`와 함께 `images/`, `files/`에 내부 자산을 복사하며, Markdown 안의 내부 업로드 URL은 상대 경로로 재작성한다. Import는 이 구조를 역으로 읽어 자산을 미디어 저장소에 다시 올리고 본문 경로를 새 업로드 URL로 매핑한다.
## 2026-06-01 v1.5.17 — 설정 토글 읽기 상태와 게시물 이동 포맷
사이트 설정 카드는 `편집`을 누르기 전에는 읽기 전용이다. 토글이 ON인 경우에도 일반 활성 스위치와 같은 검정 톤이면 조작 가능한 컨트롤처럼 보이므로, 읽기 모드 토글은 낮은 대비와 비활성 커서를 사용해 현재 값은 보여 주되 편집 전에는 조작할 수 없음을 드러낸다.
게시물 Import/Export는 Obsidian 로컬 보관과 재가져오기를 함께 고려해야 하므로, 1차 포맷은 HTML이나 JSON보다 범용성이 높은 Markdown 파일로 둔다. 제목·태그·상태·발행일·대표 이미지·SEO 같은 CMS 메타데이터는 YAML frontmatter에 넣고, 본문은 기존 Markdown을 최대한 그대로 유지한다. 자산 백업 기준은 v1.5.18에서 게시물별 로컬 번들 구조로 정정했다.
## 2026-06-01 v1.5.16 — TOC를 읽기 위치 표시 장치로 확장
게시글 TOC는 클릭 이동만 지원하면 긴 글에서 현재 위치를 다시 파악하기 어렵다. 본문 스크롤 위치를 기준으로 현재 H1~H3 제목을 계산해 TOC 항목에 강조 상태를 주고, 항목이 많아 TOC 영역 안에서 넘칠 때는 활성 항목이 보이도록 내부 스크롤을 보정한다. 모바일에서는 TOC 자체를 숨기는 기존 결정을 유지해 본문 아래에 불필요한 긴 목차가 붙지 않게 한다.
댓글 작성 버튼은 빈 입력 상태에서 활성화되어도 서버 요청 전에 차단되기만 하므로 사용자에게 실행 가능한 동작처럼 보인다. trim 기준 입력값이 있을 때만 댓글·답글 등록 버튼을 활성화해 화면 상태와 실제 저장 조건을 맞춘다.
## 2026-05-27 v1.5.15 — 기본 사용자 아이콘 표시 범위 통일
헤더의 사용자 메뉴 버튼은 로그인 여부를 여는 진입점이기도 하므로 비로그인 상태에서 `?`를 표시하면 오류나 누락처럼 보인다. 아바타가 없는 로그인 회원과 비로그인 방문자 모두 같은 사람 아이콘을 쓰도록 통일해 버튼 의미를 명확하게 유지한다.
## 2026-05-27 v1.5.14 — 모바일 TOC와 미디어 일괄 작업 정리
게시글 TOC는 데스크톱 오른쪽 사이드바에서는 읽기 이동성을 높이지만, 모바일에서는 사이드바가 본문 아래로 내려가 읽기를 마친 뒤에야 보이므로 실효성이 낮다. 따라서 모바일 폭에서는 TOC를 숨기고 본문 흐름을 짧게 유지한다. 미디어 라이브러리는 폴더·검색·필터가 이미 운영 단위이므로, 전체 선택도 현재 표시 결과만 대상으로 두어 실수로 숨겨진 항목까지 선택하지 않게 했다. 선택 삭제는 기존 잠금 정책을 유지해 게시물·페이지나 회원 프로필에서 쓰이는 파일은 제외한다.
## 2026-05-27 v1.5.13 — TOC 이동감과 위치 보정
목차 링크를 기본 해시 이동에 맡기면 브라우저가 즉시 점프하고, 고정 헤더와 제목의 위치 관계에 따라 제목이 화면 위쪽에 붙거나 일부 가려질 수 있다. 게시글 TOC는 읽는 흐름을 돕는 장치이므로 클릭 이벤트를 직접 처리해 부드러운 스크롤로 이동하고, 제목 요소 자체에는 상단 헤더 높이와 여백을 반영한 scroll margin을 둔다.
## 2026-05-27 v1.5.12 — 게시글 상세 오른쪽 사이드바 TOC 전환
추천 사이트는 탐색 보조 영역이지만, 게시글을 읽는 순간에는 외부 이동보다 본문 안에서 빠르게 이동하는 편의가 더 중요하다. 따라서 게시글 상세에서는 오른쪽 사이드바의 Recommended 영역을 숨기고, 본문 H1~H3 제목에서 추출한 TOC를 같은 위치에 표시한다. 본문 렌더러와 TOC가 같은 앵커 ID 생성 규칙을 공유하도록 공통 유틸을 두어 제목 중복이나 한글 제목에서도 링크가 안정적으로 맞게 했다.
## 2026-05-27 v1.5.11 — 멤버 상세 보기/수정 모드 분리
멤버 상세 화면은 운영자가 상태를 확인하는 시간이 더 많고, 권한·이메일·관리자 노트 같은 값은 실수로 바뀌면 영향이 크다. 따라서 기존 회원 상세는 먼저 읽기 전용 상태만 보여주고, 명시적으로 `수정하기`를 누른 뒤에만 편집 컨트롤을 노출한다. 저장 버튼도 실제 변경이 생긴 뒤에만 활성화해 “저장할 내용이 있는지”를 버튼 상태로 알 수 있게 했고, 저장 결과는 다른 관리자 화면과 같은 우측 상단 토스트로 통일한다.
## 2026-05-27 v1.5.10 — 권한 UI와 글 목록 스캔성 보정
권한 변경은 서버에서 거부되더라도 사용자가 바꿀 수 있는 것처럼 보이면 운영 실수와 혼란이 생긴다. 따라서 소유자 본인 강등, 관리자의 소유자·관리자 조작처럼 서버 규칙상 막히는 상황은 멤버 상세 화면에서도 셀렉트를 비활성화하고, 등급 변경 권한 자체가 없는 세션은 일반 텍스트로만 보여 준다. 글 목록은 필터만으로는 특정 글을 빠르게 찾기 어려우므로 검색을 추가하고, 대표 이미지는 제목 옆 작은 썸네일로만 보여 목록 높이를 키우지 않으면서 이미지 존재 여부를 알 수 있게 했다. 페이지 HTML 문서는 VS Code식 `!`+Tab 습관을 살려 빈 본문에서 기본 골격을 빠르게 채우도록 했다.
## 2026-05-27 v1.5.9 — 페이지 통계와 추천 사이트 메타데이터 확장
고정 페이지는 HTML 랜딩 페이지처럼 단독 URL로 쓰이기 때문에 게시물처럼 조회 추이를 볼 수 있어야 한다. 일반 Nuxt 페이지는 기존 클라이언트 통계 플러그인으로 pageSlug를 함께 보내고, 원문 HTML 문서 모드는 Nuxt 앱이 실행되지 않으므로 서버 미들웨어에서 GET 조회를 직접 기록한다. 추천 사이트는 URL 자체보다 운영자가 지정한 짧은 문구와 썸네일이 더 명확한 경우가 있으므로, 기존 파비콘 fallback은 유지하되 대체 텍스트와 썸네일 URL을 선택적으로 저장하게 했다.
## 2026-05-27 v1.5.8 — 소유자 권한 보호와 멤버 목록 등급 표시
소유자는 시스템을 복구할 수 있는 최상위 권한이므로 본인 계정을 관리자 이하로 직접 낮출 수 없게 한다. 기존의 마지막 소유자 보호는 동시에 여러 사용자가 권한을 바꾸는 상황을 막기 위한 장치로 유지하고, 이미 소유자가 0명이 된 개발 DB는 마이그레이션으로 가장 오래된 관리자 계정을 소유자로 되돌릴 수 있게 했다. 멤버 목록은 운영자가 등급을 빠르게 스캔해야 하므로 별도 열을 추가하지 않고 기존 상태 열에 등급을 먼저 보여주며, 활성 상태는 기본값이라 숨기고 비활성만 보조 상태로 표시한다.
## 2026-05-26 v1.5.7 — 페이지 형식 선택과 IP 기록 보정
페이지 형식 선택은 현재 모드와 관계없이 되돌릴 수 있어야 하므로 설정 패널에서 항상 표시한다. 일반 텍스트 모드는 HTML 문서가 아니어서 HTML 자산 업로드가 의미 없으므로 해당 업로드 UI는 HTML 문서 모드에서만 표시한다. 멤버 정보의 접속 IP는 프록시 뒤에서 기본 `getRequestIP`가 비어 저장될 수 있으므로, 로그인·회원가입·댓글 활동 등 회원 활동 기록에는 `x-forwarded-for`를 포함한 요청 IP 조회를 공통으로 사용한다.
## 2026-05-26 v1.5.6 — 멤버 권한 변경을 저장 액션과 서버 규칙으로 고정
멤버 등급은 접근 권한을 바꾸는 민감한 값이므로 셀렉트 변경 즉시 저장하면 사용자가 의도하지 않은 권한 변경이 발생할 수 있다. 따라서 멤버 상세 화면의 등급 변경은 다른 기본 정보처럼 저장 버튼을 눌렀을 때만 반영한다. 서버는 화면 상태와 무관하게 소유자·관리자만 권한을 변경할 수 있게 하고, 관리자는 다른 관리자나 소유자를 조작하지 못하며 소유자·관리자 등급도 부여하지 못하게 막는다. 마지막 소유자 보호는 트랜잭션 잠금 안에서 검사해 동시에 권한을 바꿔도 소유자가 사라지지 않도록 한다.
## 2026-05-26 v1.5.5 — 멤버십 공개 기준을 VIP 등급으로 고정
로그인 회원은 댓글 작성을 위해 필요한 기본 사용자 범위라서 멤버십 콘텐츠의 공개 기준으로 쓰기에는 너무 넓다. 따라서 `members` 상태 게시물은 단순 세션 존재가 아니라 `vip`, `admin`, `owner` 등급에만 노출되도록 바꾼다. `vip`는 관리자 권한은 없지만 멤버십 콘텐츠 접근 권한을 가진 등급이며, 관리자 멤버 상세 화면에서 직접 지정할 수 있게 했다.
## 2026-05-26 v1.5.4 — 게시물·페이지 공개 상태를 다시 확장
게시물은 초안·발행·예약만으로는 운영자가 숨겨야 하는 글과 회원에게만 보여야 하는 글을 구분하기 어렵다. 따라서 `private`는 공개 화면에서 완전히 숨기는 상태로 되살리고, `members`는 회원 전용 공개 상태로 추가한다. 이후 v1.5.5에서 이 회원 전용 공개 기준은 로그인 여부가 아니라 VIP 이상 등급 기준으로 확정했다.
고정 페이지도 운영 전이지만 HTML 랜딩 페이지를 붙여넣어 쓰는 구조라 공개 전 확인이나 비공개 보관 상태가 필요하다. 페이지는 게시물과 달리 멤버십 공개 요구가 아직 없으므로 `draft`, `published`, `private`만 두고, 공개 API와 HTML 문서 미들웨어는 `published`만 응답하게 정리한다.
## 2026-05-26 v1.5.3 — 고정 페이지 기본값을 HTML 랜딩 페이지 작성에 맞춤
고정 페이지는 운영에서 아직 본격 사용 전이고, 앞으로는 단일 랜딩 페이지 HTML을 붙여넣어 공개 URL에서 원문 HTML로 응답하는 용도가 중심이다. 따라서 새 페이지의 기본 형식은 HTML 문서 모드로 바꾸고, 기존 Markdown 모드는 실제 사용 가능성이 낮으므로 관리자 UI에서 `일반 텍스트`로 명확히 표시한다. 페이지 대표 이미지는 공개 원문 HTML 응답과 연결되지 않으므로 제거하고, HTML 안에서 필요한 이미지는 기존 미디어 업로드 API로 파일을 올린 뒤 URL을 현재 커서 위치에 삽입하는 방식으로 정리한다. 미디어 사용 여부는 페이지 본문 문자열 안에 저장된 URL도 검사하므로, HTML 코드에 업로드 URL이 포함되면 페이지 사용처로 추적된다.
## 2026-05-26 v1.5.2 — 페이지 작성 화면을 게시글 작성 화면과 통일
고정 페이지는 랜딩 페이지 작성 용도로 확장되므로 일반 관리자 폼보다 게시글 작성과 같은 집중형 전체 화면 에디터가 더 적합하다. 게시글과 페이지의 작성 화면이 다르면 저장 위치, 설정 위치, 본문 입력 방식이 매번 달라져 운영 피로가 커진다. 따라서 페이지 작성/수정도 상단 툴바와 오른쪽 접이식 설정 패널을 쓰고, 페이지 형식 선택과 URL·대표 이미지·삭제 액션은 설정 패널로 모은다. 기본 콘텐츠 입력도 게시글과 같은 Markdown-first 에디터를 사용해 작성 경험을 통일한다.
## 2026-05-26 v1.5.1 — 고정 페이지에 원문 HTML 문서 모드 추가
고정 페이지는 아직 운영에서 본격 사용 전이므로 구조를 크게 바꿀 수 있다. 랜딩 페이지처럼 한 주소에서 단일 `index.html` 문서를 보여주는 목적에는 Nuxt 컴포넌트 안의 `v-html`보다 서버에서 `text/html`로 원문을 응답하는 방식이 맞다. 따라서 페이지에는 `render_mode`를 두고, 기본 Markdown 모드는 기존 경로를 유지하되 `html_document` 모드만 서버 미들웨어가 `/pages/:slug` 요청을 가로채 저장된 HTML을 그대로 반환한다. 이 모드는 관리자만 저장하는 신뢰 콘텐츠를 전제로 한다.
## 2026-05-26 v1.5.0 — 글쓰기 태그 입력을 검색형 선택으로 정리
게시물 태그는 여러 개 저장되지만 관리자 글 목록에서는 운영자가 빠르게 분류를 읽는 것이 우선이므로 첫 번째 태그만 대표 태그로 표시한다. 글쓰기 화면은 직접 입력 흐름을 유지하되, 메인 태그처럼 이미 관리자가 등록한 태그는 다시 타이핑하지 않아도 되도록 오른쪽 트리거를 선택 드롭다운으로 바꾼다. 기존 태그는 이름과 슬러그 부분 일치로 추천해 `no` 입력만으로 `note` 같은 태그를 찾고, 방향키와 Enter로 추가할 수 있게 한다. 태그별 색상은 관리 화면에서 이미 운영자가 지정하는 분류 신호이므로 글쓰기 배지와 글 목록 대표 배지에도 같은 색상을 반영한다.
## 2026-05-22 v1.4.7 — 라이브 모드 인라인 마크다운 직렬화
라이브 편집 영역은 화면에 ``·`` 등으로 표시되지만, blur 시 저장 경로가 `textContent`만 읽으면 `**`·`*` 마커가 빠진다. 문단 이동 시 이전 블록이 blur·commit 되므로 방향키만으로도 서식이 사라진 것처럼 보였다. `readEditableTextFromElement`가 DOM 인라인 노드를 마크다운으로 다시 직렬화하도록 수정한다.
인용 블록은 표준 `>` 문법을 유지하되, 첫 줄에 `> [!bg=yellow]`처럼 옵션 줄을 둘 수 있게 한다. 새 fenced block을 추가하지 않으면 기존 마크다운과 호환되고, 콜아웃에서 이미 쓰는 배경 프리셋을 공유할 수 있다. 소스에서 라이브로 전환할 때는 커서 줄에 포커스만 두고 스크롤하지 않는 경로가 있어 화면 위치가 어긋났으므로, 해당 전환에서는 대상 줄을 중앙에 가깝게 스크롤한다.
## 2026-05-22 v1.4.6 — 사이트 설정 이미지 저장 흐름 통일
관리자 사이트 설정은 섹션별 `편집` 후 `저장`으로 반영되는 컨셉이므로, 로고 업로드도 DB를 즉시 갱신하지 않고 업로드된 파일 URL만 폼에 반영한 뒤 기타 설정 저장 시 함께 저장하도록 정리한다. 홈 커버는 공개 라이트·다크 테마에 따라 이미지 톤이 크게 달라질 수 있어 기존 라이트 이미지를 기본값으로 유지하면서 다크 전용 URL을 별도 컬럼으로 추가한다. 다크 이미지가 없으면 기존 이미지로 fallback해 기존 설정과 공개 화면 동작을 유지한다.
## 2026-05-22 v1.4.5 — 게시물 작성자 기준 편집 링크
공개 게시글 상세의 편집 버튼은 단순히 “관리자 로그인 여부”가 아니라 실제 글쓴이인지로 판단해야 한다. 현재 운영은 관리자 1인 작성 전제지만, 멤버·권한 구조가 이미 분리되어 있으므로 게시물에 `author_id`를 명시해 현재 로그인 회원 ID와 비교하는 방식으로 정리한다. 기존 게시물은 owner/admin 계정이 정확히 1개일 때만 backfill해 잘못된 작성자 배정을 피하고, 새 게시물은 관리자 세션의 사용자 ID를 작성자로 저장한다.
## 2026-05-21 v1.4.2 — 관리자 레이아웃 라이트 테마 격리
공개 사이트의 라이트/다크 테마는 `html[data-theme]`와 CSS 변수로 전역에 적용된다. 같은 앱 안의 관리자 화면이 이 변수를 그대로 상속하면, 공개 화면을 다크모드로 둔 상태에서 관리자 네비게이션 입력처럼 별도 배경색을 명시하지 않은 폼 컨트롤이 어두운 색으로 바뀌어 관리 UI 가독성이 깨진다. 관리자 로그인은 별도 다크 인증 화면으로 유지하되, 로그인 이후 `admin-layout`은 운영 도구 성격에 맞춰 라이트 UI로 고정한다. 다만 글쓰기 에디터는 별도 입력 UX가 있으므로, 폼 컨트롤 `color-scheme` 재정의는 `admin-layout--light-controls`가 붙은 일반 관리자 화면에만 적용한다.
## 2026-05-21 v1.4.1 — 라이브 모드 임베드 즉시 프리뷰 전환
단독 URL 붙여넣기가 임베드 작성의 기본 흐름이 되면서 라이브 모드의 별도 URL 입력 카드는 같은 값을 다시 입력하게 만드는 중복 UI가 됐다. 임베드 블록은 즉시 실제 프리뷰로 보여주고, iframe 때문에 일반 텍스트처럼 커서를 둘 수 없는 문제는 프리뷰 래퍼 자체를 포커스 가능한 블록으로 만들어 삭제·아래 줄 추가 키보드 조작을 받도록 했다. 같은 문제가 업로드 비디오·오디오·파일 카드에도 적용되므로 공통 프리뷰 카드 동작으로 확장한다. iframe·audio 컨트롤 등이 내부 포커스를 가져갈 수 있으므로 hover/focus 삭제 버튼을 둔다. 선택된 프리뷰 카드에서 방향키가 페이지 스크롤로 빠지지 않도록 `ArrowUp`·`ArrowDown`은 이전/다음 편집 줄 포커스 이동으로 처리한다. 임베드 전용 왼쪽 핸들은 전체 블록 공통 정책이 아니므로 제거한다.
## 2026-05-21 v1.4.1 — 라이브 모드 제목 Enter 처리 보정
제목 블록에서 Enter를 누를 때 일반 블록 아래 삽입 로직만 타면 현재 제목 편집 값이 먼저 저장되지 않거나 다음 포커스가 원문 줄처럼 보이는 상태로 이어질 수 있었다. 제목 Enter는 제목 마크다운 줄과 아래 빈 줄을 한 번에 반영해, 제목은 유지하고 다음 빈 문단으로 자연스럽게 이동하도록 분리했다.
## 2026-05-21 v1.4.1 — 임베드 저장 형식 단독 URL 통일
단독 URL 한 줄을 자동 임베드로 해석하게 되면서 같은 외부 링크가 `https://...`와 `:::embed` fenced block 두 형식으로 새로 저장될 수 있었다. 작성자가 원본 마크다운을 읽을 때 혼선이 생기지 않도록 새 임베드 삽입, 라이브 편집, 레거시 블록 변환의 저장 형식은 단독 URL 한 줄로 통일하고, 기존 `:::embed` 콘텐츠는 렌더링 호환만 유지한다.
## 2026-05-21 v1.4.1 — 미디어 업로드 크기 한도 분리
관리자 게시물 미디어 업로드 API가 `MAX_FILE_SIZE`(기본 10MB) 하나만 쓰고 있어 동영상 업로드가 413으로 실패했다. 아바타·로고 등 이미지 전용 한도는 유지하고, `POST /admin/api/uploads`만 비디오·오디오·문서별 환경 변수(`MAX_VIDEO_FILE_SIZE` 등)로 검사하도록 분리했다. 에디터 미디어 모달에는 최대 용량 안내와 413 토스트를 추가했다.
## 2026-05-20 v1.4.0
### 미디어 선택과 단독 URL을 작성 흐름에 연결
비디오·오디오·파일 블록을 템플릿만 삽입하면 작성자가 업로드 URL을 직접 복사해야 하므로 이미지/갤러리와 UX가 맞지 않는다. 기존 미디어 모달을 확장해 파일 종류별 선택·업로드 후 fenced block URL과 표시 메타를 자동으로 채우도록 했다. 외부 임베드는 작성자가 `:::embed`를 기억하지 않아도 되도록 단독 `http(s)` URL 한 줄을 같은 임베드 블록으로 해석한다.
### 외부 임베드를 플랫폼별 표시 정책으로 분리
YouTube는 본문 폭 16:9 영상이 자연스럽지만 X/Twitter 공식 iframe은 내부 카드 최대 폭이 고정되어 있어 전체 폭 iframe으로 두면 오른쪽 공백이 커지고 내용이 잘릴 수 있다. X/Twitter와 Mastodon은 소셜 카드로 보고 좁은 폭 중앙 정렬을 적용한다. Mastodon은 인스턴스별 공개 게시물 URL 뒤에 `/embed`를 붙이는 표준 경로를 우선 사용하고, 공식 embed 스크립트와 같은 `postMessage` 높이 요청으로 긴 글이 잘리지 않게 한다. 다만 인스턴스 정책에 따라 iframe 표시가 실패할 수 있으므로 링크 fallback 정책은 유지한다.
### 미디어 Prose 블록을 fenced block 렌더링으로 연결
비디오·오디오·파일은 기존 이미지/갤러리처럼 업로드 URL을 본문 마크다운에 저장하는 방식이 관리와 이식성이 가장 단순하다. 따라서 `:::video`, `:::audio`, `:::file` fenced block을 추가하고, 표시용 메타는 `url=`, `title=` 같은 키값으로 둔다. 공개 화면은 카드형 컴포넌트가 렌더링하고, 관리자 에디터는 슬래시 명령으로 기본 템플릿을 삽입해 이후 업로드/선택 UI 확장과 분리한다.
## 2026-05-20 v1.3.7
### NAS 마이그레이션 명령을 npm 없는 호스트 기준으로 보정
NAS 운영 호스트는 Docker Compose만 있고 Node/npm이 없을 수 있다. 운영 DB 마이그레이션은 앱 빌드 도구가 아니라 배포 호스트에서 실행하는 운영 절차이므로, `npm run db:migrate:prod:*`만 안내하면 실제 NAS에서 막힌다. Docker Compose와 DB 컨테이너의 `psql`만 사용하는 `scripts/migrate-production-db.sh`를 추가해 호스트 npm 설치 여부와 무관하게 상태 확인, baseline, 미적용 SQL 실행을 처리하도록 했다.
## 2026-05-20 v1.3.6
### NAS 운영 마이그레이션 적용 이력 도입
운영 NAS에서 몇 번 SQL까지 적용했는지 파일명만으로 추적하면 누락과 중복 실행을 사람이 기억해야 한다. `schema_migrations` 테이블에 적용 완료 파일을 기록하고, 운영용 `db:migrate:prod`는 기록이 없는 파일만 실행하도록 했다. 이미 운영 중인 DB에는 과거 적용 기록이 없으므로, 기존 스키마가 감지되면 001부터 자동 실행하지 않고 baseline 기록을 요구해 데이터 변경 SQL의 의도치 않은 재실행을 막는다.
## 2026-05-20 v1.3.5
### 관리자 로그인·대시보드 차트·통계 보관 후속
v1.3.4 통계 확장 이후 운영에서 로그인 쿠키·클라이언트 번들·차트 조회 오류가 겹쳐 후속 정리가 필요했다. 세션 쿠키는 공통 유틸로 묶고, 통계 상수는 `analytics-shared`로 분리했다. 대시보드는 기간별 `trends` 차트와 접속자 목록 가독성을 맞췄다. 저장 용량은 일별 집계는 누적 원본으로 두고 방문자 해시만 32일 초과 시 정리한다.
## 2026-05-15 v1.1.18
### 에디터 미디어 UX·발행일·수정일 표시 설정
이미지 삽입을 미디어/업로드 버튼으로 나누면 워드프레스 대비 단계가 많아지므로, 툴바는 `이미지`·`갤러리`만 두고 모달에서 라이브러리(기본)와 업로드 탭을 전환한다. 본문 이미지 너비 옵션은 렌더·운영 복잡도 대비 사용 빈도가 낮아 툴바·블록 편집 UI에서 제거한다(기존 `{width=…}` 마크다운은 파싱만 유지). 발행일은 관리자 목록에서 시·분까지 보여 주고, 발행 후 수정이 있으면 `site_settings.show_post_updated_at`이 true일 때만 보조 줄로 수정 시각을 노출해 공개 상세와 동일한 정책을 쓴다.
## 2026-05-15 v1.1.16
### 게시물 상태 단순화·초안 첫 저장·발행 글 편집 UX
접근 권한별 공개 범위는 추후 별도 기능으로 두고, 지금은 `private`를 없애 모두 초안·발행(`published_at`으로 예약 여부 판별)만 쓴다. 제목 없이 저장이 막히던 문제는 신규에 임시 슬러그와 서버 측 빈 제목 `(제목 없음)` 보정으로 푼다. 이미 발행된 글에서 사이드바만 바꾸면 툴바가 `Publish`로 바뀌던 것은 폼 상태만 본 탓이므로, 서버에 반영된 게시 형태를 따로 두어 `Update`·자동 저장 여부를 맞춘다.
## 2026-05-15 v1.1.15
### 초안 서버 자동 저장·이탈 가드·목록 헤더
신규 글도 초안·비공개면 DB 행 없이 입력만 두면 복구가 불가능하므로 기존 글과 동일하게 디바운스 `POST`로 첫 행을 만든 뒤 편집 URL로 옮긴다. 초안·비공개는 이후에도 자동 저장되므로 내부 이동 시 미저장 확인 모달은 발행·예약처럼 자동 저장이 없는 경우에만 둔다. 디바운스 직후 이탈로 놓치는 한 번의 변경은 라우트 가드에서 타이머를 취소하고 즉시 `POST`/`PUT` 플러시로 보완한다. 글 목록은 Ghost류처럼 필터를 «새 글» 왼쪽에 붙여 한 눈에 조작 단위를 모은다.
## 2026-05-15 v1.1.14
### 관리자 글쓰기 툴바·저장 정책을 Ghost에 맞춤
로컬 자동 저장 복원은 저장된 DB 초안과 새 글 작성 UX가 충돌한다. 초안은 서버 디바운스 저장으로 `Draft`/`Saving...`/`Draft - Saved`를 맞추고, 발행·예약 글은 자동 저장 없이 `Update` 한 번에만 반영한다. 툴바는 상태별 `Publish`·`Update`·`Unpublish`·`Unschedule`과 `Published ↗`·`Scheduled` 툴팁으로 의도를 분명히 한다.
## 2026-05-15 v1.1.13
### 상단 메뉴 1뎁스·추천 사이트·파비콘 프록시
사이드바 상단 네비는 운영상 루트와 그 직속 자식만이면 충분하고, 그보다 깊은 트리는 편집·표시 모두 부담이 된다. 저장·공개 트리 조립·관리자 드래그에서 한 단계로 막는다. 우측 Recommended는 하드코딩 대신 `location=recommended` 평면 행으로 두어 메뉴 관리 한 화면에서 다루게 하고, 외부 링크는 호스트만 추출해 Google Favicon CDN URL을 쓰면 별도 스크래핑 없이 아이콘을 얻을 수 있다(내부 경로는 생략).
## 2026-05-15 v1.1.12
### 상단 메뉴 편집: 드롭 구역 시각 구분과 계층형 개요 번호
행 위·중·아래만으로는 형제 순서 이동과 부모 변경(하위 편입)이 한눈에 구분되기 어렵고, 평면 행 번호(2,3,4…)는 부모·자식 관계와 맞지 않아 혼란스럽다. 드래그 중에는 파란 끝선·앰버 링과 짧은 한글 캡션으로 의미를 고정하고, 개요 열은 `1`, `2.1`, `2.2`처럼 트리 깊이에 맞춘 표기로 바꾸며 라벨 들여쓰기를 키워 구조를 읽기 쉽게 한다.
## 2026-05-15 v1.1.11
### 관리자 상단 네비를 평면 드래그 편집으로 통일
`하위` 전용 추가 버튼과 중첩 테이블은 트리 깊이가 늘수록 조작이 어색하고 공개 사이드바와 다른 시각 계층을 준다. 항목은 모두 `상단 메뉴 추가`로 만든 뒤, 한 테이블에서 들여쓰기만으로 깊이를 보여 주고, 행의 위·가운데·아래 드롭 구역으로 형제 사이 끼움과 하위 편입을 나누면 Ghost류 아웃라이너와 비슷한 자유도를 유지하면서 UI는 단순해진다.
## 2026-05-15 v1.1.10
### 관리자 사이트 설정을 전용 전체 화면으로 분리
공개 블로그 설정은 항목이 늘어날 예정이라 목록형 관리자 레이아웃 안에 두면 세로 공간과 시선 분산이 커진다. Ghost Admin처럼 설정만 별도 전체 화면으로 열고 좌측 앵커 내비와 우측 긴 스크롤을 두면 확장(타임존·공지·가져오기/보내기·스팸)을 같은 패턴으로 쌓을 수 있다. 따라서 `/admin/settings`에서는 기본 관리자 사이드바를 숨기고, 닫기·ESC와 문서 스크롤 잠금으로 집중도를 맞춘다.
## 2026-05-15 v1.1.9
### 추천 글을 저장 필드로 분리
홈 Featured와 목록의 번개 표시는 최신 글 여부가 아니라 운영자가 명시한 추천 상태여야 한다. 기존처럼 첫 번째 글을 추천처럼 보이게 하면 추천 의도와 최신순 정렬이 섞이므로, 게시물에 `is_featured` 필드를 추가하고 글쓰기 사이드바 토글로 관리하도록 했다. 추천 글이 없으면 홈 Featured 영역도 숨겨 빈 운영 상태에서 불필요한 섹션이 보이지 않게 한다.
### 관리자 글 목록 필터를 클라이언트 우선으로 도입
현재 관리자 글 목록은 전체 글을 한 번에 조회하는 구조라 상태·태그·정렬 필터를 클라이언트에서 먼저 적용해 변경 범위를 줄였다. 목록 규모가 커지면 같은 필터 기준을 `/admin/api/posts` 쿼리 파라미터로 옮길 수 있도록 상태 키와 태그 필터 계산을 별도 함수로 분리했다.
## 2026-05-15 v1.1.8
### 태그 순서 저장을 드롭 즉시 자동화
메인 태그 정렬은 드래그 자체가 명확한 저장 의도를 가진 조작이므로 별도의 `정렬 저장` 버튼을 두면 화면의 책임이 나뉘어 보인다. 태그 추가 버튼도 화면 전체 제목 옆에 있으면 메인 태그 추가처럼 보일 수 있어, 새 태그가 기본적으로 일반 태그로 생성되는 현재 구조에 맞춰 일반 태그 섹션 헤더 오른쪽으로 옮겼다. 순서 저장 중에는 추가 드래그를 잠시 막아 서버 순서와 화면 순서가 어긋나지 않게 한다.
## 2026-05-15 v1.1.7
### 사이트 로고 파일명을 교체마다 고유하게 저장
사이트 로고 업로드는 미디어 라이브러리에 `시스템` 폴더 메타로 남지만, 기존 구현은 항상 `/uploads/system/logo.webp`와 `/uploads/system/favicon.png`를 덮어썼다. 운영 브라우저와 파비콘 캐시는 같은 URL의 이미지를 오래 보관할 수 있어 파일이 바뀌어도 이전 이미지처럼 보일 수 있다. 따라서 로고와 파비콘은 업로드마다 고유 파일명으로 저장하고 사이트 설정 URL 자체를 갱신한다. 현재 사이트 설정에서 참조 중인 로고·파비콘은 미디어 라이브러리에서 사용 중인 파일로 표시해 실수로 이름을 바꾸거나 삭제하지 못하게 했다.
## 2026-05-15 v1.1.6
### 일반 태그도 검색 없이 보이는 관리 화면
메인 태그는 공개 카테고리 노출용이고 일반 태그는 게시물 분류 보조용이므로, 새 태그를 무조건 메인 태그로 올리면 공개 노출 의도가 섞인다. 다만 일반 태그를 검색해야만 볼 수 있으면 방금 만든 태그가 저장되지 않은 것처럼 느껴진다. 따라서 새 태그는 일반 태그 기본값을 유지하되, 관리자 태그 화면에서 일반 태그 전체 목록을 배지 형태로 항상 보여주고 필요할 때 메인 태그로 전환하도록 정리했다. 배지는 최근 사용순을 기본으로 하되 운영 판단에 따라 많이 사용순·이름순으로 바꿀 수 있게 했다.
## 2026-05-15 v1.1.5
### 운영 업로드 파일을 런타임 볼륨에서 직접 제공
Nuxt 운영 빌드는 `public/`을 빌드 시점에 `.output/public`으로 복사해 정적 파일로 제공한다. 반면 Docker 운영 업로드는 컨테이너 실행 중 `/app/public/uploads` 볼륨에 기록되므로, 새 파일이 `.output/public` 스냅샷에 없으면 업로드 직후 이미지가 깨져 보일 수 있다. 업로드 파일은 사용자 콘텐츠이자 런타임 데이터이므로 빌드 산출물에 의존하지 않고 `/uploads/**` 요청을 `public/uploads`에서 직접 스트리밍하도록 결정했다.
## 2026-05-15 v1.1.4
### 관리자 멤버 썸네일 업로드 경로 분리
회원 프로필 썸네일은 관리자 계정인지 일반 회원인지와 무관하게 회원 자산이므로 `/uploads/members/avatars`에 저장해야 한다. 관리자 멤버 편집 화면이 공용 게시물 이미지 업로드 API를 사용하면 `/uploads/posts`에 저장되어 미디어 분류와 썸네일 생명주기 규칙이 어긋난다. 회원 설정 업로드와 관리자 멤버 업로드가 같은 검증·WebP 변환·1:1 크롭 로직을 쓰도록 공통 유틸로 분리하고, 관리자 멤버 화면은 회원 전용 업로드 API를 사용하도록 정리했다.
## 2026-05-13 v1.1.3
### 사이드바 행 호버 배경 분리
전역 `site-panel-hover`는 패널과 텍스트 색을 `color-mix`해 라이트에서도 호버가 진하게 느껴진다. 카드·태그 목록 등 다른 패널은 기존 대비를 유지하고, 왼쪽 사이드바 네비·카테고리·테마 점만 `site-sidebar-nav-row`로 분리해 라이트에서 `#F7F4EF`로 완화했다. 다크에서는 가독성을 위해 기존과 동일한 `color-mix` 호버를 유지한다.
## 2026-05-14 v1.1.2
### 태그 없을 때 “POST” 더미 표시 제거
태그 배열이 비어 있을 때 UI 폴백으로 `POST` 문자열을 넣어 두어, 사용자는 실제 태그가 붙은 것으로 오해했다. 저장 데이터와 무관한 표시이므로 슬러그가 있을 때만 첫 태그를 노출하고 없으면 태그 영역을 렌더하지 않는다.
## 2026-05-14 v1.1.1
### 공개 문단 행간 기본값으로 복귀
문단 글자 크기만 16px(`text-base`)로 고정하고 행간은 `leading-7` 대신 Tailwind·브라우저 기본(`leading-normal` 계열)에 맡긴다.
## 2026-05-14 v1.1.0
### 관리자 제목·공개 본문 타이포 마이너 조정
관리자 글 제목 입력은 전체 화면 폼에서 과도하게 커 보일 수 있어 `text-3xl`로 낮췄다. 공개 본문 문단은 15px·좁은 행간 조합보다 `text-base`·`leading-7`로 읽기 리듬을 맞추고, 제목 블록의 고정 `mt-12`를 제거해 본문 첫 블록과의 간격을 렌더 트리에 맡긴다.
## 2026-05-14 v1.0.19
### 수정 모드에서 보이는 hard break 표식
마크다운 표준의 공백 2개 hard break는 렌더링 결과는 맞지만 textarea 수정 모드에서는 공백이 보이지 않아 일반 Enter와 Shift+Enter를 구분할 수 없다. Markdown-first 에디터가 아직 plaintext textarea 기반인 동안에는 작성자가 줄바꿈 의미를 눈으로 확인할 수 있어야 하므로, Shift+Enter는 줄끝 백슬래시 hard break를 삽입하도록 바꾼다. 렌더러는 새 백슬래시 방식과 이전 공백 2개 방식을 모두 지원해 기존 저장 콘텐츠와 호환한다.
## 2026-05-14 v1.0.18
### 빈 줄 공백 보존과 미리보기 집중 모드
일반 Enter를 단일 줄 이동으로 유지하더라도, 작성자가 의도적으로 여러 줄을 비우면 미리보기와 공개 본문에서도 그만큼의 공백이 보여야 한다. 빈 줄을 문단 경계로만 소비하면 제목 아래나 문단 사이에 넣은 여백이 모두 사라지므로, 내용 없는 줄은 다시 spacer 블록으로 렌더링해 줄 수 정보를 보존한다. 다만 기본 문단 간격은 10px로 유지해 일반 문단 흐름과 의도적 공백을 분리한다.
미리보기 모드는 작성 도구가 아니라 결과 확인 화면이므로 툴바와 카드형 패널 외곽을 숨긴다. 작성 모드 줄 번호 거터는 본문 바깥 위치 안내로만 쓰고 스크롤바는 숨겨 편집 화면의 시각 잡음을 줄인다.
## 2026-05-14 v1.0.17
### textarea 기준 문단 입력 재조정
일반 Enter를 `\\n\\n`으로 저장하면 마크다운 문단 구분은 명확하지만, 작성 화면에서는 커서가 두 줄 내려가 보여 긴 글 작성 리듬이 어색해진다. textarea 기반 편집에서는 Affine처럼 일반 Enter를 단일 줄 이동의 새 문단으로 보고, Shift+Enter만 같은 문단 안 hard break로 구분하는 쪽이 더 편하다. 따라서 일반 Enter는 브라우저 기본 입력을 유지하고, Shift+Enter만 마크다운 hard break(`공백 2개 + \\n`)를 삽입한다. 렌더러는 hard break가 있는 행만 같은 문단으로 묶고, 일반 줄은 각각 10px 간격의 문단으로 렌더링한다.
### 작성 영역 카드감 축소
본문 textarea에 외곽 보더와 배경 카드가 있으면 관리자 화면의 다른 패널과 겹쳐 편집 영역이 과하게 박스처럼 보인다. 작성 영역의 보더와 배경을 제거하고, 줄 번호 거터는 본문 흐름 바깥 absolute 영역으로 분리한다. 현재 줄 강조 액센트는 제거하고 줄 번호 자체만 위치 안내로 사용한다.
## 2026-05-14 v1.0.16
### 문단과 줄바꿈 의미 분리
Markdown-first 에디터에서 빈 줄을 그대로 spacer로 보존하면 세로 간격은 조절할 수 있지만, 글쓰기 경험이 옵시디언·고스트처럼 “문단”과 “줄바꿈”으로 나뉘지 않는다. 운영 글쓰기에서는 일반 Enter를 새 문단, Shift+Enter를 같은 문단 안 줄바꿈으로 보는 Ghost형 규칙이 더 예측 가능하다. 따라서 에디터는 일반 Enter 입력 시 `\\n\\n`을 넣고, Shift+Enter는 브라우저 기본 단일 줄바꿈을 유지한다. 렌더러는 빈 줄을 별도 spacer가 아니라 문단 경계로만 쓰며, 연속 텍스트 줄은 한 문단으로 묶어 단일 줄바꿈을 ` `로 표시한다. 문단 간 구분감은 개별 spacer가 아니라 문단 하단 24px 간격으로 통일한다.
### 미리보기 전환 후 커서 복원
`Cmd/Ctrl+E`로 미리보기를 확인한 뒤 작성 모드로 돌아왔을 때 포커스가 사라지면 긴 글을 이어 쓰는 흐름이 끊긴다. 작성 textarea의 선택 시작·끝 위치와 스크롤 위치를 기억하고, 미리보기에서 돌아올 때 같은 위치로 포커스와 선택 영역을 복원하도록 했다.
## 2026-05-14 v1.0.15
### 마크다운 빈 줄 간격 보존
Markdown-first 에디터에서는 작성자가 빈 줄을 넣어 문단 사이 호흡을 직접 조절할 수 있어야 한다. 기존 `ContentMarkdownRenderer`는 빈 줄을 파싱 단계에서 건너뛰어 1줄과 2줄의 차이가 모두 사라졌다. 빈 줄과 레거시 빈 문단 마커를 `spacer` 블록으로 렌더링해 공개 본문과 관리자 미리보기에서 작성자가 넣은 세로 간격을 보존한다. 세부 높이는 이후 본문 스타일 QA에서 조정하되, 우선 줄 수 정보가 사라지지 않는 것을 기준으로 한다.
## 2026-05-14 v1.0.14
### Markdown-first 전환 후 레거시 본문 정규화
Markdown-first 에디터로 전환한 뒤에도 브라우저 자동 저장본이나 이전 블록 에디터 상태가 배열·객체 형태로 남아 있으면, 저장 API의 `content: string` 검증에서 “게시물 입력 형식” 오류가 날 수 있다. 데이터베이스 저장 형식은 마크다운 문자열로 유지하되, 클라이언트 복원 단계와 서버 입력 검증 단계에 공통 정규화 유틸을 두어 레거시 블록 값을 마크다운으로 변환한다. 이렇게 하면 사용자가 기존 자동 저장본을 복원해도 발행 흐름이 끊기지 않고, 이후 페이지 편집 쪽도 같은 기준을 공유한다.
## 2026-05-14 v1.0.13
### Markdown 에디터 붙여넣기와 미디어 편집 보강
textarea 기반 Markdown-first 편집은 범위 선택과 복사/붙여넣기 문제를 줄였지만, 외부 웹 글을 붙여넣을 때 HTML 구조가 일반 텍스트로 무너지거나 이미지·갤러리 마크다운을 직접 고쳐야 하는 불편이 남았다. 우선 브라우저 클립보드의 `text/html`을 제목·문단·목록·링크·강조·이미지 중심의 마크다운 조각으로 변환하고, 커서가 이미지 또는 갤러리 블록 안에 있을 때 별도 편집 패널을 보여 alt·URL·너비·갤러리 순서를 수정하도록 했다. 작성과 미리보기 전환은 반복 작업이므로 `Cmd/Ctrl+E` 단축키로 접근하게 하고, 관리자 미리보기는 공개 렌더러를 쓰되 밝은 관리자 패널에 맞는 색상 변수를 별도로 고정한다. 완전한 옵시디언식 토큰 숨김 Live Preview와 표준 마크다운 파서는 더 큰 편집 엔진 선택이 필요하므로 후속 단계로 둔다.
## 2026-05-13 v1.0.12
### Markdown 에디터 줄 번호 거터
옵시디언·CodeMirror는 편집 줄 왼쪽에 줄 번호와 현재 줄 하이라이트를 둔다. 본문 편집이 textarea 단일 호스트로 바뀐 뒤에도 동일한 방향의 안내가 있으면 긴 본문에서 위치 파악이 쉬워진다. CodeMirror 수준의 **시각 줄**(줄바꿈 wrap) 단위 번호는 별도 미러 레이아웃 없이는 맞추기 어렵고, 우선 `\\n` 기준 **논리 줄** 번호와 캐럿이 속한 논리 줄의 거터 셀 배경 강조, 스크롤 동기화만 구현했다.
## 2026-05-14 v1.0.11
### 관리자 글쓰기를 Markdown-first로 전환
블록형 에디터는 이미지·갤러리 같은 카드형 입력에는 편하지만, 여러 문단 범위 선택, 외부 블로그/옵시디언 복사·붙여넣기, 다중 블록 복사 같은 기본 글쓰기 동작이 브라우저 텍스트 편집 모델과 계속 충돌했다. 저장 포맷은 이미 마크다운 문자열이므로 본문 편집의 원본도 마크다운 문자열로 되돌리고, textarea 기반 작성 모드와 공개 렌더러 기반 미리보기를 제공한다. 이미지와 갤러리는 기존 업로드·미디어 라이브러리 API를 유지하고 커서 위치에 마크다운으로 삽입한다. 옵시디언식 토큰 숨김 Live Preview는 이 기반이 안정화된 뒤 별도 단계로 확장한다.
## 2026-05-13 v1.0.10
### 관리자 블록 에디터를 v1.0.5 파일 기준으로 복원
v1.0.6부터 적용했던 다중 줄 붙여넣기 분할, Cmd/Ctrl+A로 전체 마크다운 복사, 블록 단위 범위 선택·레인 드래그·복사 가로채기 등이 실제 사용에서 어색하다는 피드백이 있어, `AdminBlockEditor.vue`는 Git 태그 `v1.0.5` 시점 내용으로 되돌렸다. Docker·부트스트랩 등 v1.0.5 이후 서버/배포 변경은 유지하고 에디터 파일만 이전 동작으로 맞춘다.
## 2026-05-14 v1.0.5
### Docker 런타임 환경 변수 우선
Nuxt `runtimeConfig`에 `process.env.*`를 직접 대입하면 Docker 이미지 빌드 시점에 값이 비어 있는 상태로 번들에 들어갈 수 있다. 운영 컨테이너는 `.env.production`을 런타임에 주입하므로, 서버 전용 비밀값과 DB 연결값은 `useRuntimeConfig()`보다 `process.env`를 우선 조회하도록 공통 유틸을 추가했다. 이 기준은 관리자 최초 로그인, 세션 서명, DB 연결, 이메일 OTP 설정에 적용한다.
## 2026-05-14 v1.0.4
### 최초 관리자 기준을 owner/admin 존재 여부로 변경
운영 DB에 일반 회원이 먼저 생성되면 기존의 “사용자 0명” 기준 부트스트랩이 더 이상 동작하지 않아 관리자 계정이 없는 잠금 상태가 생길 수 있다. 최초 관리자 필요 여부를 전체 사용자 수가 아니라 `owner`/`admin` 권한 보유자 존재 여부로 판단하고, `ADMIN_EMAIL`과 같은 일반 회원이 이미 있으면 해당 회원을 `owner`로 승격하면서 `ADMIN_PASSWORD` 기준 비밀번호 해시를 갱신해 운영 복구 경로를 보장한다. 이미 `owner`/`admin`이 있으면 환경 변수 로그인은 우회 권한으로 쓰지 않는다.
## 2026-05-14 v1.0.3
### NAS에서 Postgres init 디렉터리 Permission denied
Docker가 호스트의 `db/migrations`를 `postgres:16-alpine`의 `/docker-entrypoint-initdb.d`에 마운트할 때, NAS 파일 시스템이나 복사 시 기본 umask 때문에 디렉터리가 `700`·파일이 `600`만 되면 컨테이너 내부 `postgres` UID로는 목록을 읽지 못한다. 엔트리포인트가 `ls /docker-entrypoint-initdb.d`에서 실패하면 DB가 즉시 종료되고 `restart: unless-stopped`로 루프에 들어간다. 배포 문서에 `chmod -R a+rX db/migrations` 및 상위 경로 `a+x` 절차를 명시하고, compose에 주석으로 동일 원인을 남겨 재발 시 빠르게 대응할 수 있게 했다.
## 2026-05-14 v1.0.1
### Docker Compose 전용 네트워크 대역 명시
NAS에서 이미 실행 중인 Docker 서비스가 많으면 Docker가 자동으로 고르는 기본 브리지 네트워크 주소 풀이 기존 네트워크와 겹쳐 Compose 실행이 실패할 수 있다. 기존 네트워크를 정리할 수 없는 운영 환경을 고려해 이 프로젝트 전용 브리지 네트워크와 기본 subnet을 명시하고, 필요 시 `.env.production`의 `DOCKER_SUBNET`으로 다른 사설 대역을 지정할 수 있게 했다.
## 2026-05-14 v1.0.0
### 운영 환경의 샘플 콘텐츠 fallback 차단
개발 단계에서는 DB 없이도 화면 구조를 확인할 수 있도록 샘플 게시물 fallback이 유용했지만, 운영 환경에서 `DATABASE_URL` 누락을 샘플 콘텐츠로 숨기면 잘못된 배포를 정상 서비스처럼 보이게 만든다. 따라서 `NODE_ENV=production`에서는 DB URL이 없으면 즉시 실패하도록 바꾸고, 샘플 콘텐츠 fallback은 개발 환경의 보조 장치로만 남긴다.
### 회원 세션 비밀값 분리
회원 세션 서명값이 `ADMIN_PASSWORD`로 fallback되면 관리자 로그인 비밀번호와 회원 쿠키 서명 책임이 섞인다. 운영에서 키 회전과 사고 대응을 분리할 수 있도록 `MEMBER_SESSION_SECRET`을 필수값으로 두고, 누락 시 명확한 서버 오류를 반환한다.
### 최소 회귀 검증 스크립트 추가
현재 프로젝트에는 전용 테스트 프레임워크가 없으므로 먼저 적용 가능한 최소 자동 검증으로 JavaScript 문법 점검과 Nuxt 프로덕션 빌드를 묶었다. `npm run verify`는 이후 단위 테스트나 E2E 테스트가 추가될 때 같은 진입점으로 확장한다.
## 2026-05-13 v0.0.121
### 자동 저장 안내를 툴바로 이동
자동 저장본이 있을 때 본문 상단에 배너를 띄우면 제목·본문 입력 흐름을 가리고 시각적으로도 무겁다. 이미 툴바 상태 영역에 자동 저장 시각 안내 문자열을 표시하고 있으므로, 같은 줄에 복원과 로컬 초안 삭제(무시)만 작은 버튼으로 붙이면 기능은 유지하면서 편집 영역 침범을 없앨 수 있다.
## 2026-05-13 v0.0.120
### 발행 모달을 Ghost 설정 행 패턴에 맞춤
첫 구현은 설정 제목과 버튼이 항상 펼쳐져 있어 고스트의 `gh-publish-setting`처럼 “현재 값만 보이다가 클릭 시 옵션 노출” 흐름과 달랐다. 사용자가 제공한 마크업에 맞춰 종이비행기·시계·펼침 화살표 SVG를 그대로 쓰고, 행 단위 접기/펼침으로 요약 표시를 맞췄다. 설정 블록 외곽의 상하 보더는 제거하고 행 사이 구분선만 두어 시각적 잡음을 줄였다.
## 2026-05-13 v0.0.119
### 게시물 저장 전 최종 발행 모달 도입
우측 설정 패널의 상태 셀렉트만으로 발행/초안/비공개를 반복 전환하는 흐름은 저장 전 최종 상태를 한눈에 확인하기 어렵고 조작도 번거롭다. 저장 버튼을 눌렀을 때 고스트 스타일의 전체 화면 발행 모달을 열고, 상태(발행/초안/비공개)와 발행 시점(즉시/예약)을 버튼식으로 빠르게 선택한 뒤 최종 확정하도록 정리했다. 뉴스레터 관련 섹션은 현재 기능 범위에 없으므로 제외했다.
## 2026-05-13 v0.0.119
### 콜아웃을 옵션 메타 기반으로 확장
콜아웃은 단순 본문만 저장하면 디자인 옵션(이모지 노출 여부, 이모지 종류, 배경 톤)을 유지할 수 없어, 작성 화면과 공개 화면의 결과를 일치시키기 어렵다. 기존 fenced 문법을 유지하면서 선언부 메타(`emoji`, `bg`)를 추가해 저장 포맷 변경 범위를 최소화했다. 이 방식은 기존 `:::callout` 콘텐츠와 호환되며, 이후 색상/아이콘 프리셋이 늘어나도 본문 포맷을 다시 바꾸지 않고 확장할 수 있다.
편집 UI는 카드 내부에 옵션 컨트롤을 넣으면 실제 공개 결과와 작성 화면이 달라 보이므로, 고스트처럼 콜아웃 카드 자체는 결과 형태를 유지하고 설정은 별도 패널로 분리했다.
콜아웃 카드 자체는 테두리 장식이 과하면 본문 흐름에서 떠 보이므로 보더를 제거하고 배경 톤 중심으로 정리했다. 이모지 선택은 정해진 목록만 강제하지 않고 별도 팝업 입력을 함께 제공해 시스템 이모지 입력 흐름을 수용한다.
## 2026-05-13 v0.0.118
### 게시글 저장·삭제 액션 강조도 조정
게시글 편집 화면의 저장 버튼은 변경사항이 없을 때도 활성화되어 있어 실제 저장 필요 상태를 구분하기 어렵다. 이미 미저장 변경사항 감지 기준을 가지고 있으므로 같은 기준으로 저장 버튼 활성 상태를 제어한다. 삭제 버튼은 파괴적 액션이지만 항상 빨간색이면 편집 흐름에서 과하게 눈에 띄므로 기본 상태는 중립 톤으로 두고 hover 시에만 위험 색상으로 전환한다. 태그 삭제는 텍스트 `x` 대신 SVG 닫기 아이콘을 사용해 배지 안 정렬을 안정화한다.
## 2026-05-13 v0.0.117
### 갤러리 선택과 순서 편집 흐름 정리
갤러리는 단일 이미지 블록과 달리 여러 이미지를 한 번에 구성하는 블록이므로, 미디어 클릭 즉시 적용하면 선택을 이어갈 수 없고 실수 수정도 번거롭다. 갤러리 미디어 선택은 모달 안에서 복수 선택 상태를 유지한 뒤 확인 시점에 블록에 반영한다. 이미지 개수별 열 수를 1·2·3열로 제한해 빈 칸을 줄이고, 작성자가 시각 흐름을 직접 정할 수 있도록 갤러리 내부 이미지는 드래그로 재정렬한다. 드래그 중에는 이미지 사이 삽입 위치를 선으로 표시해 어느 위치에 들어갈지 명확히 보여준다.
## 2026-05-13 v0.0.116
### 게시글 제목 IME 입력과 목록 태그 표시 보정
게시글 제목 입력에서 한글 조합 중 Enter를 본문 포커스 이동으로 함께 처리하면 마지막 조합 글자가 본문 에디터에 들어갈 수 있다. Enter가 IME 조합 확정인지 일반 이동 명령인지 구분해 조합 중에는 본문 이동을 막는다. 게시글 목록 태그는 편집 가능한 입력과 달리 삭제 액션이 필요 없으므로, 태그 관리와 같은 배지 인식성만 유지한 읽기 전용 형태로 표시한다.
## 2026-05-13 v0.0.115
### 사이트 설정과 계정 설정 책임 분리
관리자 사이트 설정에 관리자 프로필과 비밀번호 변경을 함께 두면 사이트 메타데이터와 개인 계정 관리가 섞인다. 계정 정보는 멤버 편집 화면에서 처리하고, 사이트 설정은 이름·설명·URL·로고·저작권처럼 공개 사이트 자체에 영향을 주는 값만 남긴다. 공개 사용자 설정은 중앙 컬럼 폭이 좁아 3분할 구조가 답답하므로 요약을 상단에 둔 세로형 흐름으로 바꾸고, 로고는 텍스트 대신 1:1 이미지를 저장해 공개 로고와 파비콘에 함께 사용한다.
## 2026-05-13 v0.0.114
### 멤버 계정 작업과 사용자 설정 화면 정리
관리자 하단의 `내 프로필`은 공개 사용자 설정으로 이동하면 관리자 컨텍스트가 끊기므로, 같은 계정이라도 관리자 멤버 편집 화면으로 보내 계정 관리 흐름을 유지한다. 비밀번호 직접 변경은 이메일 전송 장애 같은 비상 상황을 위한 관리자 전용 보조 수단으로 두고, 일반 사용자 설정에서는 비밀번호 변경과 회원 탈퇴를 상시 노출하지 않고 설정 메뉴의 모달 액션으로 낮췄다. 마지막 로그인은 현재 세션 조회 때마다 갱신하면 의미가 흐려지므로, 로그인 성공 시 기존 `last_seen_*` 값을 `previous_last_seen_*`로 옮긴 뒤 현재 로그인만 갱신한다.
## 2026-05-13 v0.0.113
### 멤버 필터와 썸네일 편집 방식 정리
멤버 목록은 검색만으로는 “Gmail 사용자 제외”, “비활성 사용자”, “특정 날짜 이후 접속 없음”처럼 운영자가 자주 쓰는 조건을 표현하기 어렵다. 서버 API를 먼저 확장하지 않고 현재 화면의 회원 목록 데이터를 기준으로 클라이언트 조건 필터를 제공해 UI 흐름을 빠르게 검증한다. 멤버 상세의 썸네일은 URL 문자열보다 이미지 자체를 클릭해 등록·변경·제거하는 방식이 더 자연스러우므로, URL 입력 필드는 숨기고 요약 영역의 원형 썸네일 액션으로 책임을 옮긴다.
## 2026-05-13 v0.0.112
### 관리자 편집 화면 이탈 확인 공통화
게시글 작성 화면은 로컬 자동 저장이 있지만 서버 저장 전 변경사항을 사용자가 의식하지 못한 채 목록으로 이동할 수 있었다. 멤버 편집 화면도 같은 편집 맥락을 가지므로, 라우트 이탈은 Ghost형 공통 모달로 한 번 확인하고 브라우저 새로고침·탭 닫기는 브라우저 기본 확인에 맡긴다. 게시글에서 이탈을 승인한 경우에는 임시 자동 저장본도 함께 버려 사용자가 명시적으로 떠난 내용을 다음 진입 때 다시 제안하지 않도록 한다.
## 2026-05-13 v0.0.111
### 관리자 멤버 상세와 추가 화면 분리
멤버 목록에서 모든 편집 기능을 처리하면 목록의 스캔성이 떨어지고 권한 변경 같은 민감 액션도 너무 쉽게 노출된다. 목록은 관측과 진입에 집중하고, 개별 회원의 이름·이메일·레이블·관리자 노트는 별도 상세 화면에서 저장한다. 레이블은 아직 공개 기능에 쓰지 않지만 이후 사용자별 칭호나 분류로 확장할 수 있도록 배열 컬럼으로 두고, 신규 회원은 활동 이력이 없으므로 활동 섹션을 렌더링하지 않는다.
## 2026-05-13 v0.0.110
### 관리자 멤버 목록 정보 밀도 정리
멤버 목록에서 이름, 이메일, 접속일, 권한 변경 컨트롤을 모두 별도 컬럼으로 두면 한 사람의 정보가 가로로 흩어지고 목록이 지나치게 넓어진다. Ghost 관리자처럼 닉네임 아래 이메일, 가입일 아래 최근 활동을 함께 묶어 읽는 구조로 바꾸고, 권한 변경은 사용자를 선택한 뒤 처리하는 후속 화면의 책임으로 분리한다. 뉴스레터 지표는 이 프로젝트에 없으므로 같은 위치에는 댓글 작성 개수를 표시한다.
## 2026-05-13 v0.0.109
### 관리자 사이드바 하단 사용자 영역 정리
상단 메뉴 아래에 로그아웃이 바로 붙어 있으면 Ghost형 관리자 내비게이션의 정보 구조와 달라지고, 주요 메뉴와 세션 액션이 같은 레벨로 보인다. 로그아웃은 하단 사용자 썸네일 드롭다운으로 옮기고, 설정은 하단 아이콘으로 배치해 상단 메뉴는 콘텐츠 관리 항목 중심으로 유지한다. 멤버 항목에는 총 멤버 수를 함께 보여 관리자가 현재 규모를 즉시 확인할 수 있게 한다.
## 2026-05-13 v0.0.108
### 관리자 캔버스 높이와 사이드바 폭 정리
관리자 개별 페이지가 각자 `section` 배경과 여백을 책임하면 콘텐츠가 짧은 화면에서 우측 배경이 끊겨 보인다. Ghost 관리자처럼 레이아웃의 우측 캔버스가 기본 화면 높이와 배경을 먼저 책임지게 하고, 사이드바는 320px 고정 폭으로 맞춰 목록·설정 화면의 기준 여백을 더 여유롭게 잡는다.
## 2026-05-13 v0.0.107
### 관리자 사이드바 Ghost형 톤 전환
기능 구현이 어느 정도 갖춰진 뒤에도 관리자 첫 화면의 어두운 사이드바는 오래된 CMS 느낌을 강하게 만들었다. Ghost 관리자처럼 밝은 바탕, 낮은 대비의 활성 행, 아이콘+라벨 내비게이션으로 바꾸고, 게시글 행 우측에 새 글 작성 `+` 버튼을 두어 목록을 거치지 않고 바로 작성으로 들어가게 했다.
## 2026-05-12 v0.0.105
### 네비게이션 기본 시드 중복 방지
`017_navigation_hierarchy.sql`에서 메뉴 계층 지원을 위해 `(location,label,url)` 유니크 제약을 제거한 뒤, 기존 `005_add_navigation_items.sql`의 `ON CONFLICT DO NOTHING`은 더 이상 동일 라벨·URL 기본 메뉴 중복을 막지 못했다. 개발 DB 마이그레이션은 전체 SQL 파일을 반복 실행하므로 기본 메뉴가 새 UUID로 누적될 수 있어, 시드 삽입을 `NOT EXISTS` 조건으로 바꾸고 `019_dedupe_navigation_items.sql`에서 기존 중복을 정리한 뒤 표현식 유니크 인덱스로 재발을 막는다.
## 2026-05-12 v0.0.104
### 관리자 권한 재검증과 마지막 소유자 보호
관리자 세션 쿠키는 서명과 만료만으로는 권한 변경·회원 탈퇴 이후 상태를 반영하지 못한다. `/admin/api/*` 요청마다 DB의 현재 `owner`/`admin` 권한을 다시 확인하는 서버 미들웨어를 추가해, 권한이 내려가거나 계정이 삭제된 세션은 즉시 차단한다. 회원 탈퇴는 마지막 `owner`를 없애지 못하도록 막고, 탈퇴 시 관리자 쿠키도 함께 정리한다.
### OTP 발송 실패와 초기 owner 판정 안정화
OTP는 메일 발송에 실패했는데 DB 챌린지만 남으면 사용자가 코드를 받지 못한 채 쿨다운에 걸릴 수 있다. 새 챌린지는 먼저 만들되 발송 실패 시 즉시 삭제하고, 발송 성공 후 이전 pending 챌린지를 정리한다. 첫 회원 생성은 동시에 들어온 요청이 모두 owner로 판정되지 않도록 `users` 테이블 잠금 안에서 처리한다.
## 2026-05-12 v0.0.103
### map.md와 관리자 메뉴 화면 동기화
본문에서 마이그레이션 안내 블록을 제거했으므로 `docs/map.md` 설명에서도 해당 문구를 뺐다.
## 2026-05-12 v0.0.102
### 관리자 블록 에디터 빈 단락·키보드·슬래시 메뉴
Markdown 직렬화에서 연속 빈 줄은 사라져 중간 빈 단락을 유지하기 어렵다. 편집용으로만 `` 한 줄을 쓰고 공개 렌더러에서 동일하게 빈 단락으로 복원한다. 슬래시 팔레트는 필터 변경 시 하이라이트를 초기화하고, 긴 목록은 스크롤·`scrollIntoView`로 따라가며, `/`가 아닐 때는 위아래 키로 블록 간 커서를 옮긴다.
## 2026-05-12 v0.0.101
### 공개 primary 네비 트리 중복 방지
`parent_id`가 있는 행과 루트로 들어온 동일 id가 공존하거나, 평면 목록에 id가 중복되면 한 루프로는 같은 노드가 `roots`와 부모 `children`에 동시에 들어갈 수 있다. 서버에서 id 단위로 한 번만 쓰고, 자식으로 연결된 id는 루트 후보에서 빼서 UI 중복을 제거한다.
## 2026-05-12 v0.0.100
### EMAIL_OTP_PEPPER 문서화
운영자가 짧은 숫자만 넣는 오해를 줄이기 위해, pepper는 세션 비밀과 별도로 OTP 해시에만 쓰이는 **긴 난수 문자열**임을 배포 문서·예시 env에 명시했다.
## 2026-05-12 v0.0.99
### 헤더 검색 중앙·Resend 이메일 OTP
헤더는 좌측 브랜드·우측 계정 사이에서 검색을 시각적 중심에 두는 편이 Ghost/Thred류 기대와 맞다. 트랜잭션 메일은 외부 SMTP 대신 Resend 단일 API로 운영 부담을 줄이고, 키가 없을 때는 기존 가입 흐름을 유지한다.
## 2026-05-12 v0.0.98
### 사이드바 푸터 링크 줄바꿈·상단 네비 호버 너비
`footer` 항목이 한 줄 `flex`로만 두면 좁은 사이드바에서 가로로 삐져나간다. `nav`에 `flex-wrap`과 `min-w-0 flex-1`을 주고 링크는 줄 단위로 감싸지게 했다. 상단 네비 행은 링크가 콘텐츠 너비만 차지하면 `site-panel-hover` 배경이 짧게 보이므로 `w-full`로 행 전체와 맞췄다.
## 2026-05-12 v0.0.97
### 상단 네비 장식·현재 페이지 표시
부모 행만 점으로 두면 리프와 시각 언어가 갈린다. `before` 세로바를 통일하고, 내부 활성 경로는 브랜드 악센트로 구분했다.
## 2026-05-12 v0.0.96
### 사이드바 상단 네비 폴더 토글 UX
chevron 전용 버튼과 펼침 배경은 원본 Ghost류 UI와 어긋난다. 부모 한 줄을 단일 컨트롤로 두고 chevron·패널 높이에 전환을 줘서 끊김을 줄였다.
## 2026-05-12 v0.0.95
### 메뉴 관리 단순화와 드래그 UX
`is_visible` 체크는 “숨기기” 용도였으나 목록에 두면 모두 표시해야 한다는 운영 기대와 맞지 않아 제거하고 항상 공개로 저장한다. `is_folder`는 자식 존재로만 서버가 채운다. 드래그는 태그 관리와 같은 행 단위 시각 피드백으로 맞췄다.
## 2026-05-12 v0.0.94
### 메뉴 관리 UX와 상단 네비 트리
위치를 셀렉트로 바꾸면 실수로 상·하단을 오가기 쉽고, 인덱스 입력은 모달·레이아웃과 겹칠 때 피드백이 약하다. 미디어처럼 탭으로 영역을 나누고 순서는 드래그로 통일했다. Ghost형 상단 그룹은 `parent_id`와 공개 트리 API, 사이드바에서 chevron 접기로 맞췄다.
## 2026-05-12 v0.0.93
### 관리자 미디어 오류 표시를 토스트로
상세·폴더 모달이 `z-50`~`z-[60]`일 때 본문 상단 배너는 모달 뒤에 깔려 사용자가 API 오류(예: 동일 파일명 409)를 볼 수 없었다. `useAdminToast`로 우측 상단 고정·높은 z-index에 두어 레이아웃과 무관하게 피드백이 보이게 했다.
## 2026-05-12 v0.0.92
### 프로필 썸네일 해제와 다운로드
서버는 이미 디스크를 지우지 않지만, 설정 화면이 `PUT /api/auth/profile`로만 `avatarUrl`을 비울 때는 메타 분리가 빠져 관리자 목록과 체감이 어긋날 수 있어 `DELETE /api/auth/avatar`와 같은 `removeManagedAvatarAsset` 호출을 맞췄다. 관리자 미디어 모달에 다운로드를 넣어 원본 확인을 쉽게 했다.
## 2026-05-12 v0.0.91
### 썸네일 미사용 자산과 업로드 파일명
프로필에서 바뀐 옛 썸네일을 바로 디스크에서 지우면 관리자가 누가 올린 자산인지 추적하기 어렵다. 메타만 끊고 파일은 남겨 썸네일 탭에서 정리하도록 바꿨다. 삭제·이름 변경 차단은 `avatar_url`이 가리키는 경우로 한정했다. 게시물 업로드는 UUID 접미 대신 원본명과 넘버링으로 검색 가능성을 높였다.
## 2026-05-12 v0.0.90
### 관리자 미디어 라이브러리와 썸네일 탭 분리
게시물 이미지는 디스크상 `posts/`에 두더라도 논리 분류는 `미분류`로 통일해 `posts` 트리와 이중 표기를 없앤다. 회원 프로필 이미지는 디스크 경로는 유지하되 논리 폴더를 예약명 `썸네일`로 고정하고, 관리자 화면에서는 탭을 나눠 검색·탐색 대상을 분리했다. 썸네일 파일은 URL이 회원 콘텐츠와 직결되므로 관리자에서 임의 삭제·이름 변경이 되면 프로필이 깨지기 쉬워 API·UI에서 막는다.
## 2026-05-12 v0.0.89
### 미디어 선택 토글 가시성
네이티브 체크박스는 배경·브라우저 기본 스타일 때문에 흰 썸네일 위에서 거의 보이지 않는 사례가 있어, 동일 hit 영역을 유지한 채 명암이 큰 커스텀 토글로 바꿨다. 폴더 트리에 나오는 `posts`·`미분류` 등은 디스크 경로와 DB 메타 규칙을 문서에 적어 운영자가 혼동하지 않도록 했다.
## 2026-05-12 v0.0.88
### 관리자 미디어 선택·폴더 UX
썸네일 전체 클릭이 선택과 미리보기를 겹쳐 쓰기 어렵다. 본문 클릭은 미리보기 모달, 좌측 체크박스만 선택으로 분리했다. 폴더 추가는 사용 빈도가 낮아 상시 입력 대신 모달로 받고, 비어 있지 않은 분류 트리를 위해 폴더 삭제 API와 행 단위 삭제를 추가했다(물리 파일은 건드리지 않고 메타만 미분류로).
## 2026-05-12 v0.0.87
### 저장·로그인 버튼 기본 비활성과 글 목록 삭제 아이콘
정렬 저장·메뉴 저장은 변경이 없을 때 연속 클릭이 의미 없으므로, 서버 스냅샷과 비교해 dirty일 때만 활성화한다. 로그인은 빈 제출을 막기 위해 필수 필드가 채워진 뒤에만 버튼을 켠다. 글 목록 삭제는 보조 액션이므로 텍스트 강조 대신 휴지통 아이콘과 낮은 기본 대비, 호버 시 강조로 시선 부담을 줄였다.
## 2026-05-12 v0.0.86
### 게시물 URL 로마자화와 태그 표기 분리
게시물 슬러그는 URL 안정성을 위해 한글 음절을 로마자로 바꾸는 기존 방식을 유지한다. 반면 태그는 사용자가 입력한 한글을 그대로 쓰는 경우가 많고, 동일 로마자화를 적용하면 배지·DB `name`이 기대와 달라지므로 태그 토큰은 한글·영문·숫자와 하이픈만 정리하는 별도 정규화로 분리했다. 저장소에서는 태그 슬러그에 한글이 포함되면 하이픈을 공백으로 바꾼 문자열을 표시명으로 쓰고, 순수 라틴 하이픈 슬러그는 기존처럼 단어별 이니셜 대문자 규칙을 유지한다.
### 관리자 글 SEO 입력 단순화
공개 상세는 이미 SEO 필드가 비어 있으면 제목·요약을 메타 기본값으로 쓰므로, 관리자 폼에서 별도 SEO 제목·설명 입력을 두면 중복 편집만 늘어난다. 저장 시점에 제목·요약을 `seo_title`·`seo_description`에 동기화하고 폼에서는 `noindex`만 노출해 입력 부담을 줄였다.
### 태그 관리 피드백을 토스트로
순서 저장 등 성공 메시지를 본문 위에 블록으로 넣으면 레이아웃이 밀려 체감 품질이 떨어지므로, 네비게이션 저장과 동일하게 우측 상단 고정 토스트로 통일했다.
## 2026-05-11 v0.0.85
### 의도한 빈 문단 저장 보존
블록 에디터는 마지막 보조 빈 문단을 자동으로 유지하는 구조라서, 저장 시 모든 빈 문단을 제거하면 사용자가 의도적으로 만든 2~3줄 공백도 함께 사라진다. 이를 구분하기 위해 빈 문단 전용 마커를 저장 포맷에 도입하고, 에디터 파서와 공개 렌더러 파서가 동일하게 해석하도록 맞춰 공백 의도를 보존했다.
## 2026-05-11 v0.0.84
### 방향키 문단 이동과 슬래시 메뉴 스크롤 고정
관리자 에디터는 블록 단위 편집이므로 일반 텍스트 에디터처럼 위/아래 방향키로 인접 문단으로 자연스럽게 넘어가야 한다. 커서가 블록 경계에 있을 때만 인접 블록으로 이동하도록 보완해 기존 블록 내부 이동과 충돌을 줄였다. 슬래시 메뉴는 명령 수가 많아도 화면을 넘기지 않도록 최대 높이+내부 스크롤로 제한하고, 방향키 하이라이트 항목을 항상 가시 영역으로 자동 스크롤해 선택 맥락을 유지하도록 정리했다.
## 2026-05-11 v0.0.83
### 슬래시 메뉴 방향키 상태 유지
슬래시 메뉴 강조 인덱스를 검색어 동기화 때마다 0으로 초기화하면 방향키를 여러 번 눌러도 체감상 1회만 이동하는 것처럼 보이므로, 검색어가 바뀐 경우에만 초기화하도록 분리했다. 또한 슬래시 입력 상태가 아닌 일반 본문 블록에서는 메뉴 방향키 핸들러를 즉시 빠져나오게 해 기본 커서 이동 동작을 최대한 유지하도록 조정했다.
## 2026-05-11 v0.0.82
### 메인 태그는 강등, 일반 태그는 검색 삭제
메인 태그는 홈 카테고리 노출 자산이므로 목록에서 실수 삭제보다 일반 태그로 되돌리는 강등 동작이 안전하다고 판단했다. 반대로 일반 태그는 수량이 많아 전체 목록 노출 대신 검색 중심으로 다루고, 삭제도 검색 결과 문맥에서만 허용해 운영 화면의 복잡도를 줄였다. 태그 수정 화면의 정렬/유형 입력은 목록 액션과 역할이 겹치므로 제거했다.
## 2026-05-11 v0.0.81
### 태그 입력 IME 안정화와 메인 태그 전환 흐름
관리자 글 작성 태그 입력은 한글 조합 중 Enter 이벤트가 완성 전/완성 후로 중복 처리될 수 있어, 조합 상태에서는 태그 추가를 막고 조합 완료 후에만 확정하도록 정리했다. 또한 게시물 작성에서 자연스럽게 늘어나는 태그는 기본적으로 일반 태그로 생성하고, 카테고리로 노출할 태그만 별도 검색 후 메인 태그로 승격하도록 운영 흐름을 분리했다.
## 2026-05-11 v0.0.80
### 태그를 관리용/일반용으로 분리하고 관리용만 정렬
태그 수가 많아질수록 모든 태그에 순번을 강제하면 운영 비용이 커지므로, 홈페이지 카테고리로 쓰는 태그만 `managed`로 분리해 순서를 관리하고 나머지는 `general`로 분리했다. 관리용 태그는 드래그 앤 드롭으로 순서를 바꾸고 일괄 저장 API로 반영해 중복 숫자 입력 문제를 제거했다.
공개 `GET /api/tags`는 관리용 태그만 반환하도록 바꿔, 카테고리 노출 목적과 일반 배지 태그 목적이 섞이지 않게 했다.
## 2026-05-11 v0.0.79
### 최초 사용자 관리자 부트스트랩 전환
관리자 계정을 환경변수로 고정하면 실제 운영에서 초기 세팅 흐름이 불명확하고, 관리자 프로필/권한 관리가 회원 데이터와 분리되어 확장성이 떨어진다. 따라서 최초 사용자가 회원가입을 시도하는 시점에 `관리자 등록` 모드로 안내하고, 첫 계정에 `is_admin=true`를 부여하는 부트스트랩 방식으로 전환했다. 관리자 로그인도 동일한 `users` 인증 체계를 사용하도록 맞춰, 관리자/회원 계정 모델을 일원화했다.
관리자 화면에서 썸네일/이름을 바로 수정할 수 있도록, 관리자 로그인 시 회원 세션도 함께 발급해 기존 회원 프로필 API를 재사용하는 방향을 선택했다.
권한은 향후 기능 확장을 위해 `owner`/`admin`/`member` 3단계로 먼저 분리하고, 현재 단계에서는 관리자 멤버 화면에서 권한 값을 변경할 수 있게 준비했다.
## 2026-05-11 v0.0.79
### 댓글 아바타/좋아요/상대시간 표시 정렬
댓글 영역은 텍스트 중심 구조만으로는 SNS형 피드백 흐름이 약해 참여 지표를 확인하기 어려웠다. 작성자 썸네일과 좋아요 토글을 기본 액션으로 배치하고, 시간 표기는 최근 24시간 동안 상대 시간으로 보여 즉시성을 높였다. 24시간 이후에는 날짜로 전환해 장기 글에서도 타임라인 문맥을 유지한다.
## 2026-05-11 v0.0.78
### 관리자 미디어에서 회원 썸네일 가시성 복구
회원 썸네일을 미디어 목록에서 완전히 제외하면 운영자가 업로드 결과를 확인하거나 정리할 수 없어 관리성이 떨어진다. 경로 분리는 유지하되 관리자 미디어에서는 `회원/썸네일` 카테고리로 조회되도록 바꿔, 일반 콘텐츠 미디어와 논리적으로 구분하면서도 관리 화면에서 추적 가능하게 했다.
## 2026-05-11 v0.0.77
### 회원 설정 썸네일 표시 방식 전환
설정 화면에서 썸네일 URL 문자열을 직접 보여 주는 방식은 실제 사용자에게 의미가 낮고, 업로드 결과를 직관적으로 확인하기 어려웠다. 프로필 카드에서 아바타 미리보기와 이미지 변경/제거 액션을 바로 제공해 확인-수정 흐름을 단순화했다.
## 2026-05-11 v0.0.76
### 회원 썸네일 중앙 1:1 강제 크롭
회원이 세로형/가로형 이미지를 올려도 헤더와 설정 화면의 아바타는 동일한 비율이어야 UI가 안정적이므로, 업로드 시점에 중앙 기준으로 1:1 정사각형 크롭을 강제했다. 이렇게 하면 클라이언트별 개별 크롭 로직 없이 서버 저장본 자체가 일관된 아바타 규격을 가진다.
## 2026-05-11 v0.0.75
### 회원 썸네일 최소 해상도/설정 방어 강화
회원 썸네일은 너무 작은 원본이 올라오면 헤더/설정 화면에서 품질 저하가 크게 보이므로 최소 해상도 제한을 추가했다. 또한 운영 환경 변수 오입력으로 리사이즈/품질 값이 비정상이어도 업로드가 깨지지 않도록 서버에서 값 범위를 보정(clamp)해 안정성을 높였다.
## 2026-05-11 v0.0.74
### 회원 썸네일 업로드 표준화(WebP + 리사이즈)
회원 아바타는 사용자 입력 원본 포맷과 해상도가 제각각이므로, 업로드 시점에 서버에서 WebP로 통일 변환하고 최대 가로/세로를 제한해 저장하기로 했다. 이 방식은 저장 공간과 전송 용량을 줄이고, 너무 큰 원본 이미지 업로드로 인한 렌더링·트래픽 비용 증가를 줄이면서도 기존 URL 분리 정책(`/uploads/members/avatars/`)을 유지할 수 있다.
## 2026-05-11 v0.0.73
### 회원 썸네일 생명주기 정리
회원 아바타는 교체가 빈번해 파일이 누적되기 쉬우므로, 업로드 성공 후 이전 회원 전용 썸네일 자산을 자동 정리하도록 했다. 또한 회원이 직접 썸네일을 제거하거나 탈퇴할 때도 동일한 정리 로직을 재사용해 고아 파일과 불필요한 `media_metadata` 레코드가 남지 않게 했다.
## 2026-05-11 v0.0.72
### 회원 썸네일 미디어 분리
회원 썸네일은 운영자(작가) 콘텐츠 제작용 미디어와 목적이 다르므로, 업로드 경로를 `/uploads/members/avatars/YYYY/MM`로 분리했다. 관리자 미디어 목록에서는 해당 경로를 숨겨, 작가용 미디어 라이브러리와 회원 프로필 자산이 섞이지 않도록 했다.
## 2026-05-11 v0.0.71
### 회원 UX를 헤더 중심으로 전환
기존 헤더는 로그인 여부와 무관하게 Anonymous 메뉴를 고정으로 보여 실제 로그인 상태가 사용자에게 전달되지 않았다. 구독 버튼 대신 우측 아바타만 남기고, 로그인 상태에서는 설정/로그아웃 메뉴를 제공해 계정 액션을 한 위치로 정리했다. 비로그인 상태에서는 기존 Sign up/Sign in을 유지한다.
### 회원 설정/활동 추적과 관리자 멤버 관측
회원 기능이 들어오면서 운영 관점에서 사용자 정보와 활동 추적이 필요해졌다. `users`에 `avatar_url`, `last_seen_at`, `last_seen_ip`를 추가하고 로그인/세션조회/댓글작성 시 최근 활동을 갱신한다. 관리자는 `/admin/members`에서 닉네임, 이메일, 최근 접속, IP, 댓글 수를 확인해 운영 판단을 할 수 있다.
### 닉네임 유니크 정책
사용자 설정에서 닉네임 변경 시 중복 체크가 필요하므로 DB 레벨에서 `lower(username)` 유니크 인덱스를 도입했다. 기존 중복 데이터로 마이그레이션이 막히지 않도록, 인덱스 생성 전 중복 닉네임은 `-2`, `-3` 접미사를 붙여 자동 정리한 뒤 인덱스를 생성한다.
## 2026-05-11 v0.0.65
### 통합 검색 모달과 `GET /api/search`
헤더 검색은 장식이 아니라 Ghost류 UX로 `/` 단축키·모달·태그·게시물 섹션 구분이 필요했다. `INPUT`/`TEXTAREA` 등에 포커스가 있을 때는 브라우저 입력과 충돌하지 않도록 `/`를 무시한다. 검색은 저장소 `searchPublicContent`에 모아 `LIKE` 대신 `position(lower(q) in lower(column))`로 부분 일치를 구현해 `%`·`_` 이스케이프 이슈를 줄였다. 저자(author) 검색은 현재 도메인 모델에 없어 제외했다.
## 2026-05-11 v0.0.63
### Tailwind 엔트리 단일화
`@nuxtjs/tailwindcss` 기본 `cssPath`는 `assets/css/tailwind.css`인데 저장소에 해당 파일이 없으면 모듈이 패키지 내 `tailwind.css`를 `nuxt.options.css` 앞에 끼워 넣는다. 프로젝트는 이미 `main.css`에 `@tailwind`와 커스텀 `@layer`를 두고 있어 두 엔트리가 겹치면 유틸·레이어 순서가 기대와 달라질 수 있다. `tailwindcss.cssPath`를 `main.css`로 고정하고, JIT `content`에 composables·modules·plugins를 포함해 클래스 수집을 보강했다.
## 2026-05-11 v0.0.62
### 인증 폼 다크 스타일이 안 보이던 현상
`layout/page.vue`의 `text-ink`는 본문에 전달되지만, 폼 컨트롤은 UA 스타일로 `color`를 상속하지 않는 경우가 많아 다크 배경에서 입력 글자와 `currentColor` SVG가 사실상 사라질 수 있다. 전역 `.auth-form-input`으로 텍스트·캐럿·placeholder·WebKit autofill 글자색을 고정하고, 토글 버튼은 SFC `scoped` 스타일로 동일하게 맞췄다. `color-scheme: dark`는 네이티브 컨트롤 테마를 맞추기 위해 섹션에 추가했다.
## 2026-05-11 v0.0.61
### 인증 폼 비밀번호 토글 아이콘화
보기/숨기기 텍스트는 좁은 모바일에서 시각적 잡음이 되고 다국어·아이콘 일관성도 떨어져, Material 스타일 단일 경로 SVG(눈 열림·가림)를 공통 컴포넌트로 두었다. `aria-label`은 필드명(`field-name`)을 받아 회원가입의 확인 필드와 구분한다.
## 2026-05-11 v0.0.60
### 홈 Featured 모바일 스크롤·화살표 상태
가로 오버플로 트랙은 기본적으로 스크롤 가능하지만, 카드 전체가 링크일 때 브라우저가 세로 제스처에 가깝게 해석하거나 체인 스크롤이 나는 경우가 있어 `touch-pan-x`와 `overscroll-x-contain`으로 가로 우선·부모 스크롤 전파를 줄였다. 화살표는 스크롤 한계에서 의미 없는 클릭을 막기 위해 `scrollLeft`와 `scrollWidth - clientWidth` 비교로 `disabled`를 두고, 레이아웃 변화에 맞추기 위해 `ResizeObserver`를 함께 썼다.
## 2026-05-11 v0.0.59
### Nuxt `#internal/nuxt/paths` Node 해석 오류
Nuxt 3.21과 `@nuxt/vite-builder`는 SSR 엔트리에서 `#internal/nuxt/paths`를 롤업 외부 모듈로 남기는데, 동일 경로의 `paths.mjs` 템플릿은 기본적으로 VFS에만 있어 디스크 파일이 없다. Node는 프로젝트 루트 `package.json`의 `imports`로만 서브패스를 해석하므로, 템플릿을 디스크에 쓰도록(`write: true`) 훅하는 로컬 모듈과 루트 `imports` 매핑을 추가했다. `nitro-server` 경로만으로 브리지하면 `nitropack/runtime` 쪽 내부 specifier가 끌려와 단독 해석이 깨지므로, Nuxt가 생성하는 `paths.mjs` 본문을 그대로 두는 방식을 택했다.
## 2026-05-11 v0.0.58
### 중앙 본문과 우측 사이드 가로 넘침
그리드에 `lg:px-5` 등 패딩이 있는데 열 정의가 `287px + 720px + 287px`로 합이 `max-width`와 맞춰져 있으면, 패딩을 제외한 실제 가용 폭보다 열 합이 커지고 `main`에 `width: 720px`가 걸려 있으면 중앙 열이 줄어들지 못해 오른쪽으로 삐져 나간다. 중앙 트랙을 `minmax(0,1fr)`로 두고 본문은 `max-width: 720px`로만 제한했으며, 열 사이 `column-gap`으로 우측 사이드와의 간격을 명시했다. 좌측 메뉴를 접었을 때는 `gap`이 왼쪽에 빈 여백을 만들지 않도록 끄고, 대신 본문에만 우측 패딩을 준다.
## 2026-05-11 v0.0.57
### 사이드바 하단 푸터 여백
Thred 참고 레이아웃에서 본문 블록은 `pl-4` 등으로 호흡이 있는데, 좌측 사이드 푸터만 `px-1`로 두어 푸터 링크가 시각적으로 왼쪽 벽에 붙어 보였다. 푸터는 스크롤 영역과 동일한 체감 밀도가 나오도록 `px-4` 이상으로 올리고, 우측 사이드 카피라이트 줄에도 소폭 `pr`을 맞춰 패널 경계와의 간격을 통일했다.
## 2026-05-11 v0.0.56
### 헤더 좁은 데스크톱 폭에서의 밀집 완화
`lg` 직후(약 1024~1280px)에서 검색창이 고정 470px이면 3단 그리드와 헤더 액션이 같은 뷰포트 안에서 경쟁해 아이콘과 버튼이 시각적으로 붙는다. 검색창은 `flex-1`과 구간별 `max-w`로 축소 가능하게 하고, 헤더·본문 그리드에 `lg`~`xl` 수평 패딩을 복구해 Thred형 3열을 유지하면서도 호흡을 확보했다.
## 2026-05-11 v0.0.55
### 공개 레이아웃 모바일 분기
`lg` 미만에서는 3열 그리드 대신 세로 흐름으로 본문을 먼저 보여 주고, 오른쪽 사이드는 본문 아래로 내린다. 왼쪽 내비는 화면 폭을 줄였을 때 본문을 밀어내지 않도록 고정 슬라이드 패널로 띄우고, 백드롭 클릭·Escape·헤더 토글로 닫을 수 있게 했다. 데스크톱에서는 기존처럼 그리드 3열과 스티키 사이드바를 유지한다.
## 2026-05-11 v0.0.54
### 공개 인증 화면 가독성과 입력 피드백 보정
회원가입/로그인은 현재 백엔드 인증 연동 전 단계이므로, 사용자가 실제 동작 상태를 오해하지 않도록 화면 피드백을 더 명확히 보여주는 것이 우선이라고 판단했다. 회원가입은 모바일 우선 여백과 카드 패널 레이아웃으로 읽기 흐름을 정리하고, 로그인 화면은 오류 메시지와 안내 메시지를 분리해 의미가 섞이지 않게 했다.
로그인 입력에서는 비밀번호 보기/숨기기 토글을 추가해 모바일 환경에서도 오입력을 줄일 수 있게 했다. 회원가입 2단계와 로그인 화면에는 상호 이동 링크를 보강해 사용자 흐름이 한 화면에서 끊기지 않도록 정리했다.
## 2026-05-11 v0.0.53
### 게시물 공유 모달 UI
게시물 상세의 제목 오른쪽 공유 버튼은 단순 아이콘만 두지 않고, 모달에서 공유 미리보기 카드와 채널별 링크를 제공하도록 확장했다. 사용자가 외부 공유 전 게시물 정보(썸네일·제목·요약)를 한번 확인할 수 있고, 링크 복사까지 같은 컨텍스트에서 끝낼 수 있어 Thred 참고 UX와 운영 편의성을 함께 맞출 수 있기 때문이다.
### 헤더 사용자 메뉴 단순화
헤더 우측은 Account 텍스트 링크 대신 아바타 아이콘 버튼으로 전환하고, 비로그인 기준 드롭다운 메뉴에서 Sign up/Sign in만 제공한다. 다크 모드나 메뉴 열기 토글은 이미 헤더와 사이드바에 노출되어 기능이 중복되므로 사용자 메뉴에서는 제거해 정보 밀도를 낮췄다.
### 회원가입/로그인 공개 화면 초안
회원가입은 `/signup` 단일 화면에서 3단계(환영, 정보 입력, 이메일 확인) 플로우로 처리한다. 초기 단계에서는 실제 메일 인프라 연결 전이므로 3단계에서 인증 메일 재전송과 인증 완료 액션을 시뮬레이션하고, 인증 완료 후 로그인 화면으로 이동시키는 흐름을 먼저 고정한다. 로그인 화면(`/signin`)은 같은 다크 톤 레이아웃으로 맞춰 인증 화면군의 시각 일관성을 유지한다.
회원가입 스텝 인디케이터는 단계별 콘텐츠 높이에 따라 위치가 바뀌지 않도록, 화면 높이를 기준으로 한 `min-h` 레이아웃의 하단 고정 영역에 둔다. 회원가입 1단계 환영 문구는 하드코딩 대신 사이트 설정 API의 `title`, `description` 값을 사용해 추후 블로그 이름/인사말 관리 화면과 자연스럽게 연결한다.
## 2026-05-08 v0.0.52
### 목록 Featured 아이콘 정렬과 상세 메타 구분자
홈/태그 목록에서 Featured 아이콘을 제목 텍스트 라인에 단순 `inline-flex`+음수 마진으로 올리면, 특정 폰트 렌더링에서 라인박스 높이가 달라져 카드 높이가 미묘하게 흔들릴 수 있다. 아이콘을 `h-4 w-4` 고정 박스로 만들고 `items-center`로 정렬해 제목 줄 높이를 안정화했다. 게시물 상세의 제목 아래 메타 정보는 원본 스킨처럼 `/` 구분자를 매번 수동으로 넣지 않고, 래퍼에서 `after` 규칙으로 일관되게 출력하도록 통일했다.
## 2026-05-08 v0.0.51
### 사이드바 고정 높이와 발행일 포맷
`lg+` 그리드에서 `items-start` 때문에 사이드바 박스 높이가 콘텐츠만큼만 잡히면 내부 `flex-1` 스크롤 영역이 늘어나지 않아 푸터가 상단 블록 바로 아래에 붙는다. 데스크톱에서 열 높이를 `h-[calc(100vh-57px)]`(및 동일 `max-h`)로 고정해 flex 컬럼 안에서 푸터를 열 하단에 두었다. 공개 피드·상세의 발행일은 `formatPostDate`로 `YYYY.MM.DD`를 통일하고 `