메뉴 관리 개편, 추천 사이트·1뎁스 네비, 우측 Recommended 연동(v1.1.13)

상단 네비는 평면 테이블·드래그로 편집하고 한 단계 하위만 허용한다.
추천 사이트 탭·location recommended·공개 API와 우측 사이드 카드·파비콘 URL 유틸을 추가한다.
문서·배포 마이그레이션 안내·관리자 레이아웃·설정 화면 등 누적 변경을 반영한다.
This commit is contained in:
2026-05-15 14:20:27 +09:00
parent 2768975752
commit ca1e17890b
24 changed files with 1509 additions and 499 deletions

View File

@@ -1,5 +1,21 @@
# 업데이트 요약
## v1.1.13
- 상단 메뉴 깊이를 한 단계로 제한하고, 추천 사이트를 DB·관리자 탭·우측 Recommended 카드(외부 파비콘 프록시)로 연결했다.
## v1.1.12
- 관리자 상단 메뉴에서 드래그 시 형제 끼움과 하위 편입을 색·문구로 구분하고, 왼쪽 번호를 계층형 개요(`2.1` 등)로 바꿨다.
## v1.1.11
- 공개 사이드바 1차 네비 비활성 표시·하위 간격을 정리하고, 관리자 상단 메뉴는 추가 후 드래그만으로 형제 순서·하위 편입을 바꾸도록 단순화했다.
## v1.1.10
- 관리자 사이트 설정 화면을 Ghost형 전체 화면(좌측 내비·스크롤 스파이·ESC 닫기)으로 바꾸고, 블로그 제목·설명은 읽기 전용 + 편집 시에만 입력하도록 정리. 상단 헤더 없이 우측 상단 고정 닫기, 사이드·본문 중앙 정렬 레이아웃을 적용한다.
## v1.1.9
- 관리자 글 목록에 상태·태그·정렬 필터와 댓글 수 표시를 추가.

View File

@@ -137,8 +137,9 @@ cp .env.example .env.production
# Docker 빌드 및 실행
docker compose --env-file .env.production up -d --build
# 기존 운영 DB를 유지한 채 새 버전을 올릴 때 추천 글 컬럼 마이그레이션 적용
# 기존 운영 DB를 유지한 채 새 버전을 올릴 때 추천 글·네비 location 마이그레이션 적용
docker compose --env-file .env.production exec sori-studio-db psql -U sori_studio -d sori_studio -f /docker-entrypoint-initdb.d/023_add_post_featured.sql
docker compose --env-file .env.production exec sori-studio-db psql -U sori_studio -d sori_studio -f /docker-entrypoint-initdb.d/024_navigation_recommended_location.sql
```
### Docker 네트워크 충돌 대응

View File

@@ -1,5 +1,29 @@
# 의사결정 이력
## 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
### 추천 글을 저장 필드로 분리

View File

@@ -8,7 +8,7 @@
|------|------|
| layouts/default.vue | 메인·목록·태그 — 3열 `gap`+중앙 `1fr`, `site-main` `max-w-[720px]`, 모바일 슬라이드 메뉴 |
| layouts/post.vue | 개별 게시물 — `default`와 동일 |
| layouts/admin.vue | 관리자 전체, 320px 밝은 Ghost형 사이드바, 우측 전체 높이 캔버스, 멤버 수 표시, 하단 사용자 드롭다운·설정, `내 프로필` 멤버 편집 이동, 게시글 `+` 새 글 진입, 글 작성/수정 화면의 전체 화면 편집 모드와 문서 스크롤 잠금 |
| layouts/admin.vue | 관리자 전체, 320px 밝은 Ghost형 사이드바(대시보드 비활성 행·사이트 보기 새 창·콘텐츠 구간 여백), 우측 전체 높이 캔버스, 멤버 수 표시, 하단 사용자 드롭다운·설정, `내 프로필` 멤버 편집 이동, 게시글 `+` 새 글 진입, 글 작성/수정 화면의 전체 화면 편집 모드와 문서 스크롤 잠금, **`/admin/settings`에서는 사이드바 숨김·본문 전체 화면·설정용 문서 스크롤 잠금** |
| layouts/page.vue | 고정 페이지 전체 화면 |
| app.vue | 공개 사이트 설정 기반 파비콘 head 링크와 기본 title template |
@@ -23,7 +23,8 @@
| 파일 | 용도 |
|------|------|
| lib/navigation-editor-tree.js | 관리자 메뉴 UI·서버 `renumberSortOrderByTree`가 쓰는 `buildNavigationEditorTree` |
| lib/external-favicon-url.js | 외부 URL 호스트 기준 Google `s2/favicons` 썸네일 URL 생성(내부 경로는 빈 문자열) |
| lib/navigation-editor-tree.js | 관리자 메뉴 UI·서버 `renumberSortOrderByTree`가 쓰는 `buildNavigationEditorTree`, 관리자 상단 네비 평면 표용 `flattenNavigationEditorWrappers` |
| lib/markdown-content-normalizer.js | 관리자 Markdown-first 전환 후 레거시 블록 배열·객체 본문 값을 저장용 마크다운 문자열로 변환 |
## Nuxt 모듈
@@ -54,7 +55,7 @@
| components/site/SiteSearchModal.vue | `Teleport`·전체 화면 딤·Tags/Posts 결과·일치 구간 강조, 열림 시 `html.site-search-open` 스크롤 잠금 |
| components/site/LeftSidebar.vue | 왼쪽 사이드바, `lg+``sticky`+고정 높이+내부 무스크롤바 스크롤, `lg` 미만은 고정 슬라이드 패널, 상단 메뉴는 `SidebarPrimaryNavList`+`provide`로 트리·펼침 상태(`sori-primary-nav-expanded`), Authors 영역은 비공개, 푸터 `footer` 링크는 `flex-wrap`·테마 버튼 `shrink-0`, 태그 카테고리·테마 점은 `site-sidebar-nav-row` 호버 |
| components/site/SidebarPrimaryNavList.vue | 상단 네비: 부모·리프 동일 `before` 막대/호버 원형, 내부 현재 경로 `--site-accent`, 행 `w-full`+`site-sidebar-nav-row` 호버(`#F7F4EF` 라이트), `inject`·`localStorage` 펼침 |
| components/site/RightSidebar.vue | 오른쪽 사이드바, 공개 사이트 이미지 로고 fallback, About 영역은 비공개, `lg+`는 고정 열 높이·스티키, 모바일은 본문 아래 전체 너비, 하단 푸터 `pr-3` |
| components/site/RightSidebar.vue | 오른쪽 사이드바, 공개 `GET /api/navigation``recommended` 카드 목록(외부 URL은 Google 파비콘 프록시 썸네일), Follow·구독 폼, About 영역은 비공개, `lg+` 스티키 |
| components/site/MainColumn.vue | 메인 화면 중앙, `lg:max-w-[720px]`로 본문 상한 |
| components/site/PostCard.vue | 목록의 게시물 카드, 대표 이미지 썸네일, 카드 hover 인터랙션, 태그는 있을 때만 메타에 표시 |
| components/site/TagHeader.vue | 태그 페이지 헤더 |
@@ -68,7 +69,6 @@
| components/admin/AdminPageForm.vue | 관리자 페이지 작성/수정 폼, 대표 이미지 선택 |
| components/admin/AdminMarkdownEditor.vue | 관리자 글 Markdown-first 에디터, textarea 기반 범위 선택·복사/붙여넣기, HTML 클립보드 마크다운 변환, Enter 새 문단·Shift+Enter 백슬래시 hard break 줄바꿈 입력, 작성 모드 왼쪽 바깥 absolute 논리 줄 번호 거터·거터 스크롤 동기화·스크롤바 숨김, 작성/미리보기 전환(`Cmd/Ctrl+E`)과 커서 복원, 작성 모드 툴바 마크다운 삽입, 미리보기 모드 툴바 숨김, 이미지·갤러리 업로드 및 미디어 라이브러리 삽입, 현재 이미지·갤러리 편집 패널 |
| components/admin/AdminBlockEditor.vue | 관리자 글 블록형 에디터, 이미지/갤러리/콜아웃/토글/임베드 블록, 콜아웃 Emoji on/off·이모지 프리셋·배경 프리셋 선택(우측 고정 설정 패널), 갤러리 복수 미디어 선택·이미지 수별 열 배치·삽입 위치 표시 드래그 순서 변경, 한글 조합 입력 처리, Shift+Enter 줄바꿈, 코드 블록 단축 변환, AFFiNE 참고 세로 막대형 블록 핸들 선택/삭제/드래그 이동과 삽입선 표시, 하단 빈 입력 블록 유지, 본문 placeholder 표시 |
| components/admin/AdminNavPrimaryBranch.vue | 관리자 상단 네비 트리(테이블·태그와 동일한 행 드래그 하이라이트, 하위·삭제) |
| components/admin/AdminTagForm.vue | 관리자 태그 생성/수정 폼(이름/슬러그/설명/색상만 편집) |
| components/admin/AdminMemberForm.vue | 관리자 멤버 추가/수정 폼(Ghost형 3분할, 요약 1fr·입력 2fr, 원형 썸네일 hover 등록·변경·삭제, 기본 정보·레이블·관리자 노트·활동 요약, 설정 메뉴의 비밀번호 변경·멤버 삭제 모달, 미저장 변경사항 이탈 확인) |
| components/admin/AdminUnsavedChangesModal.vue | 관리자 편집 화면 공통 미저장 변경사항 이탈 확인 모달(상단 40px 위치) |
@@ -115,11 +115,11 @@
| pages/admin/pages/new.vue | 페이지 작성, 저장 토스트 |
| pages/admin/pages/[id].vue | 페이지 수정, 저장/삭제 토스트 |
| pages/admin/media/index.vue | 미디어 관리, **미디어 라이브러리/썸네일** 탭, 검색은 파일명·게시물/페이지 사용처 제목만, 라이브러리: 폴더 트리·드래그 이동 등, 현재 사이트 설정 로고·파비콘은 사용 중 파일로 잠금, 썸네일: 미참조 파일 삭제·이름 변경 가능, 상세 모달(연결 회원·폴더 편집·**다운로드**) |
| pages/admin/navigation/index.vue | 메뉴 관리: 상단/하단 탭, 테이블+행 드래그(태그 메인과 동일 톤), `useAdminToast` |
| pages/admin/navigation/index.vue | 메뉴 관리: 상단/하단/추천 사이트 탭, 상단은 1뎁스 제한·평면 테이블+계층형 개요·행 드래그(위/중/아래), 하단·추천은 평면 드래그, `useAdminToast` |
| pages/admin/tags/index.vue | 태그 관리(메인 태그 드래그 정렬 자동 저장·일반 강등, 일반 태그 검색/메인 전환/삭제, 일반 태그 헤더의 태그 추가 버튼), 액션 피드백 토스트 |
| pages/admin/tags/new.vue | 태그 생성 |
| pages/admin/tags/[id].vue | 태그 수정 |
| pages/admin/settings/index.vue | 사이트 설정(이름, 설명, URL, 1:1 로고 업로드·고유 URL 파비콘 생성, 저작권 문구) |
| pages/admin/settings/index.vue | 사이트 설정 Ghost형 전체 화면: 좌측 검색·내비와 우측 본문을 **중앙 `max-w` 래퍼**에 묶고 본문은 **약 760px** 상한, 우측 상단 **고정 닫기**, 밝은 회색 배경·본문 열 흰색, 블로그 제목·설명은 읽기 전용·`편집` 시 입력·저장/취소, 기타(로고·URL·저작권) 저장, 타임존·어나운스·Import/Export·스팸은 플레이스홀더 |
| pages/admin/members/index.vue | 관리자 멤버 목록(Ghost형 테이블, 검색, 조건 필터, 멤버 추가 버튼, 닉네임+이메일, 가입일+최근 활동, IP, 댓글 수, 활동 상태 텍스트) |
| pages/admin/members/new.vue | 관리자 멤버 추가(썸네일 URL, 이름, 이메일, 레이블, 관리자 노트) |
| pages/admin/members/[id].vue | 관리자 멤버 상세/수정(회원 요약, 가입 정보, 활동 요약, 기본 정보 저장, 삭제 후 목록 이동) |
@@ -238,6 +238,7 @@
| db/migrations/002_seed_development.sql | 개발용 샘플 데이터 |
| db/migrations/003_add_tag_display_fields.sql | 태그 표시 순서와 색상 필드 추가 |
| db/migrations/004_add_site_settings.sql | 사이트 설정 테이블 추가 |
| db/migrations/024_navigation_recommended_location.sql | `navigation_items.location``recommended` 값 허용 |
| db/migrations/017_navigation_hierarchy.sql | `navigation_items``parent_id`·`is_folder`, `(location,label,url)` 유니크 제거 |
| db/migrations/019_dedupe_navigation_items.sql | 반복 마이그레이션으로 생긴 네비게이션 중복 행 정리 및 중복 방지 인덱스 |
| db/migrations/005_add_navigation_items.sql | 네비게이션 항목 테이블 추가 |

View File

@@ -318,7 +318,7 @@ components/content/
| id | UUID | Primary Key |
| label | String | 메뉴 표시 이름 |
| url | String | 내부 경로 또는 외부 URL |
| location | Enum | primary/footer |
| location | Enum | primary / footer / recommended |
| sort_order | Integer | 표시 순서 |
| is_visible | Boolean | 공개 화면 표시 여부 |
| created_at | DateTime | 생성일 |
@@ -377,7 +377,7 @@ components/content/
- `GET /api/tags` - 태그 목록
- `GET /api/search?q=` - 통합 검색(태그 `name`·`slug`, 게시물 `title`·`excerpt`·`content` 부분 일치, 각 최대 12건, 발행 게시물만)
- `GET /api/site-settings` - 공개 사이트 설정
- `GET /api/navigation` - 공개 네비게이션(`primary`는 트리·`footer`는 평면, 상세는 위 메뉴/네비게이션 절)
- `GET /api/navigation` - 공개 네비게이션(`primary`는 트리·`footer`·`recommended`는 평면, 상세는 위 메뉴/네비게이션 절)
- `POST /api/auth/signup` - 회원 가입. 본문: `username`, `email`, `password`, 선택 `emailOtp`(6자리 숫자). **`GET /api/auth/bootstrap-status``emailOtpConfigured`가 true이고 `needsAdminSetup`이 false일 때**(서버에 Resend·pepper 설정됨) `emailOtp`가 필수이며, 직전에 `POST /api/auth/email-otp/request`로 같은 이메일·`purpose: "signup"` 발송분과 일치해야 한다. 최초 관리자 부트스트랩(`needsAdminSetup: true`)에서는 `emailOtp` 없이 가입 가능. 이메일은 저장 시 소문자로 정규화한다.
- `GET /api/auth/bootstrap-status` - 최초 관리자 등록 필요 여부(`hasUsers`, `needsAdminSetup`) 및 **이메일 OTP 사용 가능 여부** `emailOtpConfigured`(서버 `RESEND_API_KEY`·`RESEND_FROM_EMAIL` 및 pepper: 비어 있지 않은 `EMAIL_OTP_PEPPER` **또는** `MEMBER_SESSION_SECRET`)를 반환한다. pepper는 OTP 6자리를 DB에 해시해 둘 때만 쓰이는 서버 비밀(긴 난문자열 권장)이다.
- `POST /api/auth/email-otp/request` - 본문: `email`, `purpose`(`"signup"` | `"password_reset"`). Resend로 6자리 OTP 메일을 보낸다. 미설정 시 503. `signup`은 이미 가입된 이메일이면 409, 최초 관리자 단계에서는 400. `password_reset`은 존재하지 않는 이메일도 동일한 성공 메시지를 반환하며(이메일 미발송), 요청 빈도 제한을 위해 DB에 더미 챌린지를 남길 수 있다. 55초 쿨다운·시간당 5회 한도. 실제 메일 발송이 실패하면 방금 생성한 챌린지는 즉시 삭제한다. 발송 성공 후 같은 이메일·용도의 이전 pending 챌린지는 무효화한다. DB 테이블 `email_otp_challenges`(마이그레이션 `018_email_otp_challenges.sql`) 필요.
@@ -552,6 +552,7 @@ components/content/
### 사이트 설정
- 관리자 사이트 설정 UI는 `/admin/settings`에서 제공한다. Ghost Admin과 유사하게 **전체 화면**으로 표시하며, 좌측 내비와 우측 본문을 **한 덩어리로 중앙 정렬**(`max-w` 래퍼, 본문 카드 영역은 약 760px 상한)하고, 페이지 배경은 밝은 회색·본문 열은 흰색으로 구분한다. 우측 본문을 스크롤하면 현재 보이는 구역에 맞춰 좌측 메뉴 활성 배경을 갱신한다. **우측 상단 고정** 닫기 버튼과 `Escape` 키로 설정 화면을 닫으며, 브라우저 히스토리가 있으면 `뒤로 가기`, 없으면 `/admin`으로 이동한다. 타임존·어나운스 바·게시물 Import/Export·스팸 필터는 현재 **메뉴·안내 카드만** 제공하고 저장 API는 연결하지 않는다. **블로그 제목·설명** 카드는 기본적으로 사이트 이름·설명을 읽기 전용으로 보여 주고, `편집`을 눌렀을 때만 입력 필드·미리보기·저장/취소가 나타난다. 제목·설명 편집 중 `Escape`는 설정 닫기 대신 편집 취소로 동작한다.
- 사이트 설정은 `site_settings` 테이블의 단일 레코드로 관리한다.
- 관리자는 사이트 이름, 설명, 사이트 URL, 로고 이미지, 저작권 문구를 수정할 수 있다.
- 로고 이미지는 1:1 비율로 저장하며 `/admin/api/settings/logo` 업로드 시 `/uploads/system/logo-YYYYMM-random.webp``/uploads/system/favicon-YYYYMM-random.png`를 고유 파일명으로 함께 생성한다. 같은 URL 덮어쓰기로 인한 브라우저·운영 정적 캐시 문제를 피하기 위해 로고 교체마다 새 URL을 저장한다.
@@ -562,12 +563,12 @@ components/content/
### 메뉴/네비게이션
- 네비게이션은 `navigation_items` 테이블로 관리한다.
- 컬럼: `parent_id`(nullable, self FK, `ON DELETE CASCADE`), `is_folder`(boolean, **자식이 있는 상단 항목이면 저장 시 서버가 true로 설정**), `location`(`primary`|`footer`), `sort_order`, `label`, `url`, `is_visible`(저장 시 항상 true로 정리). `(location, label, url)` 유니크 제약은 제거되었다.
- 컬럼: `parent_id`(nullable, self FK, `ON DELETE CASCADE`), `is_folder`(boolean, **자식이 있는 상단 항목이면 저장 시 서버가 true로 설정**), `location`(`primary`|`footer`|`recommended`), `sort_order`, `label`, `url`, `is_visible`(저장 시 항상 true로 정리). `(location, label, url)` 유니크 제약은 제거되었다.
- 동일 `location`·`parent_id`·`label`·`url` 조합은 중복 저장하지 않는다. 반복 마이그레이션으로 생긴 중복은 `019_dedupe_navigation_items.sql`이 정리하고, 표현식 유니크 인덱스로 재발을 막는다.
- `GET /api/navigation` 응답: `primary`**트리**(각 노드에 `children` 배열이 있을 수 있음, 리프는 `children` 없음). 노드 필드: `id`, `label`, `url`, `isFolder`, `isVisible`. 트리 조립 시 **동일 `id`는 한 번만 반영**하고, DB 평면 행에 `parent_id`가 있어 자식으로 붙은 항목은 **루트에 두지 않는다**(레거시·중복 행으로 인한 사이드바 이중 표시 방지). `footer`**평면** 배열(`parent_id` 없음)이며 `id`, `label`, `url`, `isVisible`만 내려간다.
- `PUT /admin/api/navigation` 요청 본문의 각 항목은 반드시 `id`(UUID)를 포함한다. 저장 시 `sort_order`서버가 위치별 트리 DFS 순으로 다시 부여한다. `parent_id``primary`에서만 허용되며 `footer` 항목은 항상 루트다. `is_visible`·`is_folder`는 요청값과 무관하게 서버에서 **항상 표시·자식 유무 기준 폴더**로 덮어쓴다.
- `GET /api/navigation` 응답: `primary`**트리**(각 노드에 `children` 배열이 있을 수 있음, 리프는 `children` 없음). 노드 필드: `id`, `label`, `url`, `isFolder`, `isVisible`. 트리 조립 시 **동일 `id`는 한 번만 반영**하고, DB 평면 행에 `parent_id`가 있어 자식으로 붙은 항목은 **루트에 두지 않는다**. **상단은 루트 직속 자식만** 트리에 붙이며(부모의 부모가 있는 행은 자식으로 무시), `footer`·`recommended`**평면** 배열(`parent_id` 없음)이며 `id`, `label`, `url`, `isVisible`만 내려간다.
- `PUT /admin/api/navigation` 요청 본문의 각 항목은 반드시 `id`(UUID)를 포함한다. 저장 시 `sort_order``primary`·`footer`·`recommended` 각각 위치별 트리(또는 평면 루트) DFS 순으로 다시 부여한다. `parent_id``primary`에서만 허용되며 **상단은 한 단계(루트→자식)만** 허용한다. `footer`·`recommended` 항목은 항상 루트다. `is_visible`·`is_folder`는 요청값과 무관하게 서버에서 **항상 표시·자식 유무 기준 폴더**로 덮어쓴다.
- URL은 `/`로 시작하는 내부 경로, `http(s)://` 외부 URL, 폴더 전용 자리 표시 `#`를 허용한다.
- 관리자 메뉴 화면은 **상단 네비게이션**·**하단 네비게이션** 탭으로 구분한다. 편집 UI는 **태그 관리 메인 태그**와 같은 테이블·`cursor-move` 행 드래그 패턴을 쓰며, 드래그는 입력·버튼 위가 아닌 행 여백·번호 열에서 시작한다. 상단은 `AdminNavPrimaryBranch`로 트리·동일 부모 내 순서 변경·하위 추가를 제공한다. 하단은 한 단계 목록만 드래그 정렬한다.
- 관리자 메뉴 화면은 **상단 네비게이션**·**하단 네비게이션**·**추천 사이트** 탭으로 구분한다. 편집 UI는 **태그 관리 메인 태그**와 같은 테이블·`cursor-move` 행 드래그 패턴을 쓰며, 드래그는 입력·버튼 위가 아닌 행 여백·개요 열에서 시작한다. 상단은 `buildNavigationEditorTree` 결과를 `flattenNavigationEditorWrappers`로 한 테이블에 평면 표시하고, **같은 행의 위 1/3·가운데·아래 1/3**에 드롭하면 각각 대상 **앞**(동일 부모)·**하위**(대상의 자식, **루트 행에만**)·**뒤**(동일 부모)로 이동한다. **상단은 루트→자식 한 단계만** 허용하며(하위 행에는 가운데 드롭이 형제 앞·뒤로 대체됨, 이미 하위가 있는 항목은 다른 항목의 하위로 넣을 수 없음), 드래그 중 하이라이트는 **형제 앞·뒤**를 파란 가로 끝선·연한 파란 배경, **하위 편입**을 앰버 링·연한 앰버 배경으로 구분하고, 개요 열에 `앞에 끼움` / `뒤에 끼움` / `하위로 넣기` 짧은 문구를 덧붙인다. 개요 열 표기는 `1`, `2.1`, `2.2`, `3`처럼 깊이별 계층 번호로 보이며, 라벨 열은 깊이만큼 `padding-left`로 들여쓴다. 자기 자신·자기 하위로의 편입은 거부한다. 하단·추천 사이트는 평면 목록만 드래그 정렬한다. 추천 사이트는 공개 홈 **오른쪽 사이드바** Recommended 영역에 카드로 노출되며, `https://` URL은 클라이언트에서 호스트를 뽑아 Google Favicon 프록시(`https://www.google.com/s2/favicons?domain=…`) 이미지로 표시할 수 있다(내부 경로만 있으면 아이콘 생략).
- `parent_id` / `is_folder` 컬럼이 DB에 없을 때 저장은 실패한다. `npm run db:migrate:dev``017_navigation_hierarchy.sql`을 적용해야 한다(저장 API는 해당 경우 한국어 안내 메시지를 반환할 수 있다).
- 공개 왼쪽 사이드바 상단은 `SidebarPrimaryNavList`로 렌더링한다. **하위가 있는 노드**는 한 줄 `button`으로, **행 전체(이름·왼쪽 세로 데코·chevron) 클릭**으로 접기/펼친다. 부모·리프 모두 왼쪽 장식은 **기본 세로 막대(`--site-line`)**, **호버 시에만** 리프와 동일하게 **작은 원형**으로 전환한다. **내부 경로**(`/`로 시작, `//`·`http(s)` 제외)이고 현재 `route.path`와 정규화한 경로가 같으면 장식 색을 **`--site-accent`(브랜드 오렌지)**로 둔다. 외부 URL은 비교하지 않는다. 펼침 상태는 `localStorage``sori-primary-nav-expanded`에 저장된다. 상단·리프 링크·부모 버튼 행은 **`w-full`**로 `site-sidebar-nav-row` 호버 배경이 가로 전체를 쓴다(라이트 `#F7F4EF`, 다크는 `site-panel-hover`와 동일한 `color-mix` 패턴).
- 관리자 메뉴 관리 화면에서 저장 API로 보내는 직렬화 결과가 서버에서 불러온 직후와 동일하면 `메뉴 저장` 버튼이 비활성화된다.

View File

@@ -1,7 +1,32 @@
# 업데이트 이력
## v1.1.13
- 상단 네비: 하위 1뎁스만 허용(서버 검증·공개 트리 조립·관리자 드래그·이미 하위가 있는 항목의 하위 편입 금지).
- `navigation_items.location``recommended` 추가(마이그레이션 `024_navigation_recommended_location.sql`), 관리자 메뉴에 추천 사이트 탭·공개 API `recommended`·우측 사이드 카드 목록.
- 외부 링크 파비콘 표시용 `lib/external-favicon-url.js`(Google `s2/favicons` 프록시 URL).
- 패키지 버전 `1.1.13`로 갱신.
## v1.1.12
- 관리자 상단 메뉴: 드롭 구역을 파란 끝선(형제 앞·뒤)·앰버 링(하위)과 개요 열 캡션으로 구분, 개요 번호를 `2.1`형 계층 표기로 변경·라벨 들여쓰기 확대.
- 패키지 버전 `1.1.12`로 갱신.
## v1.1.11
- 공개 사이드바 1차 네비: 부모·하위 사이 `mt-2` 간격, 비활성 세로 표시를 `color-mix(in srgb, var(--site-line) 88%, var(--site-panel) 12%)` 톤으로 정리.
- 관리자 상단 메뉴: `하위` 버튼 제거, `flattenNavigationEditorWrappers` 단일 테이블+행 위·중·아래 드롭으로 순서·부모 자유 변경, `AdminNavPrimaryBranch.vue` 제거.
- 패키지 버전 `1.1.11`로 갱신.
## v1.1.10
- 관리자 `/admin/settings`를 Ghost형 전체 화면으로 재구성(좌측 검색·앵커 내비·우측 스크롤 스파이, X·ESC 닫기, 타임존·어나운스·Import/Export·스팸 섹션은 플레이스홀더).
- 설정 경로에서 관리자 기본 사이드바를 숨기고 문서 스크롤 잠금(`admin-settings-document`)을 적용.
- 관리자 `/admin/settings`에서 상단 헤더를 제거하고 우측 상단 고정 닫기만 두며, 사이드·본문 열을 `max-w-[1120px]` 래퍼로 중앙 정렬·본문 카드 폭은 `max-w-[760px]`로 Ghost에 가깝게 맞춤.
## v1.1.9
- 관리자 사이드바 상단에 대시보드(비활성 표시)·사이트 보기(`NUXT_PUBLIC_SITE_URL` 기준 새 창)·콘텐츠 메뉴 구분 여백 추가.
- 관리자 글 목록에 상태·태그·최신순/오래된순 필터 추가.
- 관리자 글 목록 상태 표시를 배지에서 단순 텍스트 색상 기준으로 정리하고 제목 옆 댓글 수 표시 추가.
- 게시물 추천 여부(`is_featured`) 저장 필드와 글쓰기 사이드바 추천 토글 추가.