v1.7.1: 멤버 권한·추천 사이트·네비게이션 UI 개선
관리자는 VIP·멤버만 수정 가능하도록 서버·UI 권한을 정리하고 소유자 권한 이전 API와 확인 모달을 추가했다. 추천 사이트 파비콘 캐시·URL 접두사·카드 편집 UI를 보강하고 확인 얼럿을 AdminConfirmModal로 통일했다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 배포 가이드
|
||||
|
||||
> 로컬 기준 v1.6.0에서 `npm run lint`, `npm run build` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다.
|
||||
> 로컬 기준 v1.6.2에서 `npm run lint` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다.
|
||||
|
||||
## 빌드 유형
|
||||
|
||||
@@ -16,6 +16,14 @@
|
||||
|
||||
## 로컬 개발
|
||||
|
||||
### v1.6.2 참고
|
||||
|
||||
- DB 마이그레이션 `057_navigation_recommended_favicon_cache.sql` 적용이 필요하다.
|
||||
- 관리자 메뉴 > 추천 사이트 탭에서 적용 아이콘 미리보기 열과 파비콘 새로고침 버튼이 보이는지 확인한다.
|
||||
- 추천 사이트 저장 후 캐시가 없는 외부 URL 항목이 `/uploads/system/recommended-favicon-*.png`로 생성되는지 확인한다.
|
||||
- 공개 홈 Recommended 카드가 서버 캐시 파비콘을 우선 표시하는지 확인한다.
|
||||
- 운영 수동 일괄 갱신: `npm run navigation:refresh-recommended-favicons`
|
||||
|
||||
### v1.6.0 참고
|
||||
|
||||
- 추가 DB 마이그레이션은 없다.
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# 의사결정 이력
|
||||
|
||||
## 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 등록을 유지한다.
|
||||
|
||||
15
docs/map.md
15
docs/map.md
@@ -27,6 +27,8 @@
|
||||
|------|------|
|
||||
| lib/upload-size-limit.js | 관리자 미디어 업로드 종류별 최대 바이트 판별·용량 문구(서버·클라이언트 공용) |
|
||||
| lib/external-favicon-url.js | 외부 URL origin 기준 Google `gstatic.com/faviconV2` 파비콘 URL 생성(내부 경로는 빈 문자열) |
|
||||
| lib/navigation-url.js | 네비게이션 URL 접두사 분리·합성·정규화(`https://` 자동 보정, `/` 내부 경로) |
|
||||
| lib/recommended-site-icon-url.js | 추천 사이트 아이콘 URL 우선순위(수동 썸네일 > 서버 캐시 > 외부 프록시) |
|
||||
| lib/navigation-editor-tree.js | 관리자 메뉴 UI·서버 `renumberSortOrderByTree`가 쓰는 `buildNavigationEditorTree`, 관리자 상단 네비 평면 표용 `flattenNavigationEditorWrappers` |
|
||||
| lib/markdown-content-normalizer.js | 관리자 Markdown-first 전환 후 레거시 블록 배열·객체 본문 값을 저장용 마크다운 문자열로 변환 |
|
||||
| lib/markdown-block-context.js | 관리자 Markdown textarea·라이브 편집 포커스 위치 기준 이미지·갤러리·HTML 임베드·인용·콜아웃·코드·토글 블록 설정 패널 대상 판별, 코드 fenced 블록 여닫는 펜스 쌍 판별 |
|
||||
@@ -61,6 +63,7 @@
|
||||
| 파일 | 용도 |
|
||||
|------|------|
|
||||
| db/migrations/056_site_settings_post_tag_limit.sql | 사이트 설정의 게시물 태그 최대 개수 컬럼 추가 |
|
||||
| db/migrations/057_navigation_recommended_favicon_cache.sql | 추천 사이트 파비콘 캐시 URL·갱신 시각 컬럼 추가 |
|
||||
|
||||
## 서버 미들웨어
|
||||
|
||||
@@ -88,7 +91,7 @@
|
||||
| components/site/SiteAnnouncementBar.vue | 공개 사이트 상단 어나운스 배너(문구·선택 링크·hex 배경색·텍스트 정렬·닫기) |
|
||||
| components/site/SiteHeader.vue | 모든 공개 페이지 상단, 사이트 이름 텍스트 브랜드, `grid-cols-3`로 검색 패널 중앙 정렬(`md+`), 우측 사용자 아바타 드롭다운(아바타 없거나 비로그인 시 사람 아이콘), `/`·`SiteSearchModal` |
|
||||
| components/site/SiteSearchModal.vue | `Teleport`·전체 화면 딤·Tags/Posts 결과·일치 구간 강조, 열림 시 `html.site-search-open` 스크롤 잠금 |
|
||||
| components/site/LeftSidebar.vue | 왼쪽 사이드바, `lg+`는 `sticky`+고정 높이+내부 무스크롤바 스크롤, `lg` 미만은 고정 슬라이드 패널, 상단 메뉴는 `SidebarPrimaryNavList`+`provide`로 트리·펼침 상태(`sori-primary-nav-expanded`), 태그 Categories 아래 공개 `GET /api/navigation`의 `recommended` 카드 목록(대체 텍스트·썸네일 우선, 외부 URL은 Google 파비콘 fallback), Authors 영역은 비공개, 푸터 `footer` 링크는 `flex-wrap`·테마 버튼 `shrink-0`, 태그 카테고리·테마 점은 `site-sidebar-nav-row` 호버 |
|
||||
| components/site/LeftSidebar.vue | 왼쪽 사이드바, `lg+`는 `sticky`+고정 높이+내부 무스크롤바 스크롤, `lg` 미만은 고정 슬라이드 패널, 상단 메뉴는 `SidebarPrimaryNavList`+`provide`로 트리·펼침 상태(`sori-primary-nav-expanded`), 태그 Categories 아래 공개 `GET /api/navigation`의 `recommended` 카드 목록(수동 썸네일 > 서버 캐시 파비콘 > 외부 프록시 fallback), 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 | 오른쪽 사이드바, 일반 화면은 설정 기반 사이트 로고 48px 정사각형 고정·SNS Follow(프리셋·사용자 SVG, 16px 아이콘 중앙 정렬)·오른쪽 사이드 광고를 표시, 게시글 상세 데스크톱은 블로그 소개·Follow 대신 최상단 H1~H3 TOC와 TOC 아래 게시물 사이드 광고(모바일 숨김, 스크롤 위치 기반 활성 표시·내부 자동 스크롤, 세로 기준선), About 영역은 비공개, `lg+` 스티키 |
|
||||
| components/site/SiteAdSlot.vue | 사이트 설정 Ads HTML 코드 렌더링, 메인 피드·메인 인피드·오른쪽 사이드·게시물 오른쪽 사이드·게시물 본문 상단·인아티클·하단 광고 슬롯 |
|
||||
@@ -116,6 +119,7 @@
|
||||
| components/admin/AdminTagForm.vue | 관리자 태그 생성/수정 폼(이름/슬러그/설명/색상만 편집, 수정 화면 변경 저장 버튼 변경사항 기준 활성화) |
|
||||
| components/admin/AdminMemberForm.vue | 관리자 멤버 추가/수정 폼(Ghost형 3분할, 기존 회원 보기/수정 모드 분리, 요약 1fr·입력 2fr, 원형 썸네일 hover 등록·변경·삭제, 기본 정보·레이블·관리자 노트·활동 요약, 설정 메뉴의 비밀번호 변경·멤버 삭제 모달, 저장 토스트, 미저장 변경사항 이탈 확인) |
|
||||
| components/admin/AdminUnsavedChangesModal.vue | 관리자 편집 화면 공통 미저장 변경사항 이탈 확인 모달(상단 40px 위치) |
|
||||
| components/admin/AdminConfirmModal.vue | 관리자 공통 확인 모달(제목·본문 슬롯·확인/취소 버튼, primary/danger 톤) |
|
||||
|
||||
## 관리자 컴포저블
|
||||
|
||||
@@ -166,7 +170,7 @@
|
||||
| pages/admin/pages/new.vue | 전체 화면 페이지 작성, HTML 문서 기본 모드 저장, 저장 토스트 |
|
||||
| pages/admin/pages/[id].vue | 전체 화면 페이지 수정, HTML 문서/일반 텍스트 모드 저장, 설정 패널 삭제, 저장/삭제 토스트 |
|
||||
| pages/admin/media/index.vue | 미디어 관리, **미디어 라이브러리/카드 썸네일/프로필 이미지** 탭, 글·멤버 목록과 같은 검색창, 파일 직접 추가, 모든 탭 공통 전체/미사용 배지형 필터, 목록 카드 오른쪽 위 사용 개수 배지, 현재 필터 결과의 삭제 가능 항목 전체 선택·선택 삭제, 이미지·비디오·오디오·파일 종류 필터, 원본 fallback·카드 썸네일 사용 배지(카드 썸네일 탭의 카드 배지는 숨김), 대표 이미지 원본 카드 썸네일 생성·재생성, 비디오 프레임 썸네일, 폴더 트리 more vert(폴더 삭제), 검색·드래그·상세 모달 등 |
|
||||
| pages/admin/navigation/index.vue | 메뉴 관리: 상단/하단/추천 탭, 추천 사이트 대체 텍스트·썸네일 URL, 행 more vert(메뉴 항목 삭제), 드래그 정렬, `useAdminToast` |
|
||||
| pages/admin/navigation/index.vue | 메뉴 관리: 상단/하단/추천 탭, 추천 사이트는 공개와 같은 카드 목록+편집 모달(전체 URL·썸네일·파비콘 갱신), 행 more vert(삭제), 드래그 정렬, `useAdminToast` |
|
||||
| components/admin/AdminRowMoreMenu.vue | 관리자 행 more vert 메뉴(트리거·화면 기준 팝오버) |
|
||||
| composables/useAdminRowMenu.js | 관리자 행 메뉴 열림 상태·바깥 클릭 닫기 |
|
||||
| pages/admin/tags/index.vue | 태그 관리(메인 태그 드래그 정렬 자동 저장·more vert 메뉴, 일반 태그 배지 more vert 메뉴·검색/정렬, 태그 추가 버튼), 액션 피드백 토스트 |
|
||||
@@ -267,13 +271,18 @@
|
||||
| server/routes/admin/api/settings/home-cover.post.js | 관리자 메인 화면 커버 업로드 API(`/uploads/system/home-cover-YYYYMM-*.webp` 생성, `시스템` 미디어 메타 저장, 라이트·다크 슬롯용 URL 반환) |
|
||||
| server/routes/ads.txt.get.js | 사이트 설정 ads.txt 루트 text/plain 응답 |
|
||||
| server/routes/admin/api/navigation.get.js | 관리자 네비게이션 목록 API |
|
||||
| server/routes/admin/api/navigation.put.js | 관리자 네비게이션 일괄 저장 API |
|
||||
| server/routes/admin/api/navigation.put.js | 관리자 네비게이션 일괄 저장 API(추천 사이트 파비콘 캐시 보존, 캐시 없는 추천 사이트 백그라운드 최초 갱신) |
|
||||
| server/routes/admin/api/navigation/[id]/favicon-refresh.post.js | 추천 사이트 파비콘 수동 갱신 API |
|
||||
| server/utils/recommended-site-favicon.js | 추천 사이트 파비콘 fetch·`/uploads/system/recommended-favicon-*.png` 저장·일괄 갱신 |
|
||||
| server/plugins/recommended-favicon-cron.js | 매월 1일 03:00(KST) 추천 사이트 파비콘 자동 갱신 |
|
||||
| scripts/refresh-recommended-favicons.js | 추천 사이트 파비콘 수동 일괄 갱신 CLI(`npm run navigation:refresh-recommended-favicons`) |
|
||||
| server/routes/admin/api/members.get.js | 관리자 멤버 목록 API |
|
||||
| server/routes/admin/api/members.post.js | 관리자 멤버 생성 API |
|
||||
| server/routes/admin/api/members/[id].get.js | 관리자 멤버 상세 API |
|
||||
| server/routes/admin/api/members/[id].put.js | 관리자 멤버 기본 정보 수정 API(회원 전용 썸네일 교체·제거 시 메타 연결 분리) |
|
||||
| server/routes/admin/api/members/[id]/avatar.post.js | 관리자 멤버 썸네일 업로드 및 즉시 반영 API |
|
||||
| server/routes/admin/api/members/[id]/role.put.js | 관리자 멤버 권한 변경 API(`owner`/`admin`/`vip`/`member`, 관리자 상호 조작 차단, 마지막 소유자 보호) |
|
||||
| server/routes/admin/api/members/[id]/ownership-transfer.post.js | 소유자 권한 이전 API(대상→소유자, 요청자→관리자) |
|
||||
| server/utils/content-schema.js | Zod 콘텐츠 스키마 |
|
||||
| server/utils/sample-content.js | 샘플 콘텐츠 저장소 |
|
||||
| server/utils/admin-auth.js | 관리자 세션 쿠키 인증 유틸리티 |
|
||||
|
||||
18
docs/spec.md
18
docs/spec.md
@@ -555,7 +555,7 @@ components/content/
|
||||
> 최종 저장 크기는 `AVATAR_MAX_WIDTH`, `AVATAR_MAX_HEIGHT` 중 작은 값을 사용해 `N x N` 정사각형으로 맞춘다.
|
||||
> `AVATAR_WEBP_QUALITY`는 1~100 범위로 보정하며, 최대 해상도 설정이 최소 해상도보다 작으면 서버에서 최소값 이상으로 자동 보정한다.
|
||||
> 회원 썸네일을 새로 업로드하거나 제거·탈퇴할 때, 이전 썸네일 URL에 대한 `media_metadata` 행은 `removeManagedAvatarAsset`으로 제거하되 **디스크 파일은 삭제하지 않는다.** 관리자 미디어 **썸네일** 탭에서 미사용 파일을 확인·삭제한다.
|
||||
> 관리자 미디어 화면의 **썸네일** 탭에서만 회원 썸네일을 목록·검색하며, `GET /admin/api/media` 응답에 `avatarOwner`(닉네임·이메일·마지막 접속·IP)가 포함될 수 있다.
|
||||
> 관리자 미디어 화면의 **썸네일** 탭에서만 회원 썸네일을 목록·검색하며, `GET /admin/api/media` 응답에 `avatarOwner`(닉네임·이메일·등급·마지막 접속·IP)가 포함될 수 있다. 프로필 이미지 목록 카드 배지는 등급(소유자·관리자·VIP·멤버)으로 표시한다.
|
||||
|
||||
### 업로드 파일 제공
|
||||
|
||||
@@ -595,7 +595,7 @@ components/content/
|
||||
- `POST /admin/api/media-folders` - 미디어 폴더 생성
|
||||
- `DELETE /admin/api/media-folders` - 미디어 폴더 삭제(해당 경로·하위 경로로 분류된 `media_metadata`는 `미분류`로 되돌림)
|
||||
- `POST /admin/api/uploads` - 관리자 게시물·페이지용 이미지 업로드(저장 파일명은 원본명 기반·동일 월 폴더 내 충돌 시 `이름-2` 등 넘버링, `/uploads/posts/YYYY/MM` 저장)
|
||||
- `POST /admin/api/member-avatar` - 관리자 새 회원 생성 전 썸네일 사전 업로드(`/uploads/members/avatars/YYYY/MM`, WebP 변환·중앙 1:1 크롭)
|
||||
- `POST /admin/api/member-avatar` - 관리자 멤버 썸네일 사전 업로드(신규·편집 공통, `/uploads/members/avatars/YYYY/MM`, WebP 변환·중앙 1:1 크롭). 편집 화면에서는 미리보기만 반영하고 **저장**(`PUT /admin/api/members/:id`) 시 회원에 연결한다.
|
||||
- `GET /admin/api/tags` - 태그 목록(옵션: `tagType`, `q`, `limit`)
|
||||
- `POST /admin/api/tags` - 태그 생성
|
||||
- `GET /admin/api/tags/:id` - 태그 상세
|
||||
@@ -609,12 +609,14 @@ components/content/
|
||||
- `GET /api/site-settings` - 공개 사이트 설정 조회(`showPostUpdatedAt`, 라이트·다크 홈 커버·어나운스 바·사이트 코드 필드 포함)
|
||||
- `GET /admin/api/navigation` - 네비게이션 항목 목록
|
||||
- `PUT /admin/api/navigation` - 네비게이션 항목 일괄 저장
|
||||
- `POST /admin/api/navigation/:id/favicon-refresh` - 추천 사이트 파비콘 수동 갱신(`{ cachedFaviconUrl, faviconCachedAt }` 반환)
|
||||
- `GET /admin/api/members` - 회원 목록(권한 코드, 최근 접속, 접속 IP, 댓글 수 포함)
|
||||
- `POST /admin/api/members` - 관리자 회원 생성. 본문: `username`, `email`, 선택 `avatarUrl`, `labels`, `note`. 생성된 회원은 `member` 권한이며 초기 비밀번호는 임의 해시로 저장한다.
|
||||
- `GET /admin/api/members/:id` - 관리자 회원 상세(썸네일, 이름, 이메일, 레이블, 관리자 노트, 활동 요약 포함)
|
||||
- `PUT /admin/api/members/:id` - 관리자 회원 기본 정보 수정. 본문: `username`, `email`, 선택 `avatarUrl`, `labels`, `note`. 이전 값이 회원 전용 썸네일 URL이고 새 값과 달라지면 `media_metadata` 연결을 분리한다.
|
||||
- `POST /admin/api/members/:id/avatar` - 관리자 회원 썸네일 업로드 및 즉시 반영(`/uploads/members/avatars/YYYY/MM`, WebP 변환·중앙 1:1 크롭)
|
||||
- `PUT /admin/api/members/:id/role` - 회원 권한 변경(`owner`/`admin`/`vip`/`member`)
|
||||
- `POST /admin/api/members/:id/avatar` - 관리자 회원 썸네일 업로드 및 즉시 DB 반영(레거시 API; 관리자 UI는 `member-avatar` + `PUT` 흐름 사용)
|
||||
- `PUT /admin/api/members/:id/role` - 회원 권한 변경(`owner`/`admin`/`vip`/`member`). 소유자 권한 이전은 `POST /admin/api/members/:id/ownership-transfer` 사용.
|
||||
- `POST /admin/api/members/:id/ownership-transfer` - 소유자 권한 이전(대상→소유자, 요청자→관리자)
|
||||
|
||||
- 글 발행/초안 전환은 `PUT /admin/api/posts/:id`의 `status`와 `published_at`으로 처리한다. 예약 글은 별도 enum이 아니라 `published`와 미래 시각의 `published_at` 조합이다.
|
||||
- 게시물 상태는 `draft`, `published`, `members`, `private`를 사용한다. `members`는 VIP 이상 등급 회원에게만 공개한다. 로그인은 댓글 작성을 위한 기본 회원 기능이며, 멤버십 글 공개 기준으로 사용하지 않는다. `private`는 관리자 편집 화면에서는 보이지만 공개 사용자 화면에서는 노출하지 않는다.
|
||||
@@ -782,10 +784,10 @@ components/content/
|
||||
- 네비게이션은 `navigation_items` 테이블로 관리한다.
|
||||
- 컬럼: `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`·`recommended`는 **평면** 배열(`parent_id` 없음)이다. `recommended`는 `id`, `label`, `url`, `descriptionText`, `thumbnailUrl`, `isVisible`을 내려준다.
|
||||
- `GET /api/navigation` 응답: `primary`는 **트리**(각 노드에 `children` 배열이 있을 수 있음, 리프는 `children` 없음). 노드 필드: `id`, `label`, `url`, `isFolder`, `isVisible`. 트리 조립 시 **동일 `id`는 한 번만 반영**하고, DB 평면 행에 `parent_id`가 있어 자식으로 붙은 항목은 **루트에 두지 않는다**. **상단은 루트 직속 자식만** 트리에 붙이며(부모의 부모가 있는 행은 자식으로 무시), `footer`·`recommended`는 **평면** 배열(`parent_id` 없음)이다. `recommended`는 `id`, `label`, `url`, `descriptionText`, `thumbnailUrl`, `cachedFaviconUrl`, `isVisible`을 내려준다. 관리자 `GET /admin/api/navigation`에는 추가로 `faviconCachedAt`을 포함한다.
|
||||
- `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` 행 드래그 패턴을 쓰며, 드래그는 입력·버튼 위가 아닌 행 여백·개요 열에서 시작한다. 상단은 `buildNavigationEditorTree` 결과를 `flattenNavigationEditorWrappers`로 한 테이블에 평면 표시하고, **같은 행의 위 1/3·가운데·아래 1/3**에 드롭하면 각각 대상 **앞**(동일 부모)·**하위**(대상의 자식, **루트 행에만**)·**뒤**(동일 부모)로 이동한다. **상단은 루트→자식 한 단계만** 허용하며(하위 행에는 가운데 드롭이 형제 앞·뒤로 대체됨, 이미 하위가 있는 항목은 다른 항목의 하위로 넣을 수 없음), 드래그 중 하이라이트는 **형제 앞·뒤**를 파란 가로 끝선·연한 파란 배경, **하위 편입**을 앰버 링·연한 앰버 배경으로 구분하고, 개요 열에 `앞에 끼움` / `뒤에 끼움` / `하위로 넣기` 짧은 문구를 덧붙인다. 개요 열 표기는 `1`, `2.1`, `2.2`, `3`처럼 깊이별 계층 번호로 보이며, 라벨 열은 깊이만큼 `padding-left`로 들여쓴다. 자기 자신·자기 하위로의 편입은 거부한다. 하단·추천 사이트는 평면 목록만 드래그 정렬한다. 추천 사이트는 공개 화면 **왼쪽 사이드바** Categories 아래 Recommended 영역에 카드로 노출되며, 대체 텍스트가 있으면 URL 대신 대체 텍스트를 표시하고 썸네일 URL이 있으면 Google Favicon 프록시 대신 썸네일을 표시한다. 썸네일이 없고 `https://` URL이면 클라이언트에서 origin을 뽑아 Google Favicon V2(`https://t3.gstatic.com/faviconV2?...`) 이미지로 표시할 수 있다(내부 경로만 있으면 아이콘 생략). 썸네일 로드에 실패하면 파비콘, 파비콘도 실패하면 라벨 첫 글자로 대체한다. 오른쪽 사이드바는 추천 사이트를 렌더하지 않고 광고와 게시물 목차 영역에 집중한다.
|
||||
- URL은 `/`로 시작하는 내부 경로, `http(s)://` 외부 URL, 폴더 전용 자리 표시 `#`를 허용한다. 저장 시 `uniare.sori.studio`처럼 프로토콜 없는 외부 호스트는 `https://`를 자동으로 붙인다. 관리자 추천 사이트 편집 모달은 URL 접두사(`https://`·`http://`·내부 경로) 선택과 본문 입력으로 나눈다.
|
||||
- 관리자 메뉴 화면은 **상단 네비게이션**·**하단 네비게이션**·**추천 사이트** 탭으로 구분한다. 편집 UI는 **태그 관리 메인 태그**와 같은 테이블·`cursor-move` 행 드래그 패턴을 쓰며, 드래그는 입력·버튼 위가 아닌 행 여백·개요 열에서 시작한다. 상단은 `buildNavigationEditorTree` 결과를 `flattenNavigationEditorWrappers`로 한 테이블에 평면 표시하고, **같은 행의 위 1/3·가운데·아래 1/3**에 드롭하면 각각 대상 **앞**(동일 부모)·**하위**(대상의 자식, **루트 행에만**)·**뒤**(동일 부모)로 이동한다. **상단은 루트→자식 한 단계만** 허용하며(하위 행에는 가운데 드롭이 형제 앞·뒤로 대체됨, 이미 하위가 있는 항목은 다른 항목의 하위로 넣을 수 없음), 드래그 중 하이라이트는 **형제 앞·뒤**를 파란 가로 끝선·연한 파란 배경, **하위 편입**을 앰버 링·연한 앰버 배경으로 구분하고, 개요 열에 `앞에 끼움` / `뒤에 끼움` / `하위로 넣기` 짧은 문구를 덧붙인다. 개요 열 표기는 `1`, `2.1`, `2.2`, `3`처럼 깊이별 계층 번호로 보이며, 라벨 열은 깊이만큼 `padding-left`로 들여쓴다. 자기 자신·자기 하위로의 편입은 거부한다. 하단·추천 사이트는 평면 목록만 드래그 정렬한다. **추천 사이트** 탭 목록은 공개 Recommended와 같은 **카드**(아이콘·제목·설명 또는 URL) 형태이며, 캐시 갱신 시각은 카드 오른쪽 끝에 표시한다. 추천 사이트 드래그는 카드 **위·아래 절반**에 드롭하면 각각 대상 **앞·뒤**로 삽입되며, 파란 가로 삽입선·`앞에 삽입` / `뒤에 삽입` 안내를 표시한다. 추천 사이트는 공개 화면 **왼쪽 사이드바** Categories 아래 Recommended 영역에 카드로 노출되며, 대체 텍스트가 있으면 URL 대신 대체 텍스트를 표시한다. 아이콘 우선순위는 **수동 썸네일 URL > 서버 캐시 파비콘(`cached_favicon_url`) > 외부 faviconV2 프록시**이다. 서버는 외부 `https://` URL의 파비콘을 `/uploads/system/recommended-favicon-*.png`로 저장하고 `favicon_cached_at`을 기록한다. 자동 갱신은 **매월 1일 03:00(KST)** 에 전체 추천 사이트를 대상으로 수행하며, **관리자 추천 사이트 탭**은 공개와 같은 **카드 목록**으로 미리보기하고 **편집** 버튼으로 모달에서 제목·URL·대체 텍스트·썸네일 URL 전체를 확인·수정한다. 삭제·취소(배경 클릭 포함)는 브라우저 `confirm`으로 한 번 더 확인하며, 모달에서 **적용** 전 입력 변경이 있으면 닫기 전에 안내한다. 편집 모달에서 `POST /admin/api/navigation/:id/favicon-refresh`로 파비콘을 수동 갱신할 수 있다. 네비게이션 저장 시 캐시 없는 추천 사이트는 백그라운드로 최초 캐시를 시도한다. 운영 수동 일괄 갱신은 `npm run navigation:refresh-recommended-favicons`를 사용한다. 오른쪽 사이드바는 추천 사이트를 렌더하지 않고 광고와 게시물 목차 영역에 집중한다.
|
||||
- `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로 보내는 직렬화 결과가 서버에서 불러온 직후와 동일하면 `메뉴 저장` 버튼이 비활성화된다.
|
||||
@@ -807,7 +809,7 @@ components/content/
|
||||
- 관리자 멤버 상세/추가 화면은 Ghost형 편집 화면을 기준으로 썸네일, 이름, 이메일, 레이블, 관리자 노트를 편집한다. 본문은 3분할 그리드로 요약 영역 1fr, 입력 영역 2fr 비율을 사용한다. 기존 회원 상세는 처음에는 읽기 전용 보기 모드로 현재 값만 표시하고, 상단 `수정하기`를 누르면 입력 필드·셀렉트가 편집 컨트롤로 전환된다. 저장 버튼은 수정 모드에서 기존 값과 달라진 내용이 있을 때만 활성화한다. 썸네일은 URL 입력 대신 요약 영역의 원형 썸네일 hover 액션으로 등록·변경·제거한다. 기존 회원 상세는 멤버 등급 선택, 가입 정보와 활동 요약을 표시하고, 신규 회원 화면은 활동 영역을 표시하지 않는다. 멤버 등급은 셀렉트 변경 즉시 저장하지 않고 저장 버튼을 눌렀을 때 기본 정보와 함께 반영한다. 소유자 본인이나 관리자가 다른 관리자·소유자를 보는 등 서버에서 변경 불가한 경우 등급 셀렉트는 비활성화하고, 등급 변경 권한이 없는 세션에서는 등급을 일반 텍스트로 표시한다. 저장·업로드·비밀번호 변경 피드백은 우측 상단 관리자 토스트로 표시한다. 기존 회원 상세의 설정 메뉴는 관리자 직접 비밀번호 변경(`PUT /admin/api/members/:id/password`)과 멤버 삭제(`DELETE /admin/api/members/:id`)를 제공한다.
|
||||
- 관리자 멤버 추가/수정 화면은 저장되지 않은 변경사항이 있을 때 내부 라우트 이동을 공통 확인 모달로 막는다. 관리자 게시글 작성/수정 화면은 **서버에 이미 즉시 발행 또는 예약으로 저장된 글**에서 미저장 변경이 있을 때만 동일 방식으로 내부 이동을 막고, 초안(서버 기준)은 서버 자동 저장과 라우트 이탈 직전 플러시로 처리하여 해당 경우에는 모달을 쓰지 않는다. 브라우저 새로고침·탭 닫기는 브라우저 기본 `beforeunload` 확인을 사용한다. 게시글 화면에서 이탈을 승인하면 레거시 키 `SORI_ADMIN_POST_AUTOSAVE:*`가 있으면 삭제한다.
|
||||
- `users.member_labels`는 관리자용 문자열 배열이며, 이후 사용자별 칭호/분류 용도로 확장한다. `users.member_note`는 관리자에게만 보이는 500자 이하 메모다.
|
||||
- 관리자 멤버 권한은 `소유자(owner)`, `관리자(admin)`, `VIP(vip)`, `멤버(member)` 단계를 사용한다. VIP는 관리자 권한이 없지만 멤버십 게시물을 볼 수 있는 등급이며, 상세 화면에서 변경한다. 권한 변경은 소유자와 관리자만 가능하다. 관리자는 다른 관리자·소유자의 권한을 변경할 수 없고, 소유자·관리자 등급을 부여할 수 없다. 소유자는 모든 등급을 관리할 수 있으나 본인 권한을 직접 낮출 수 없고, 시스템에는 최소 1명의 소유자가 항상 남아야 한다.
|
||||
- 관리자 멤버 권한은 `소유자(owner)`, `관리자(admin)`, `VIP(vip)`, `멤버(member)` 단계를 사용한다. VIP는 관리자 권한이 없지만 멤버십 게시물을 볼 수 있는 등급이며, 상세 화면에서 변경한다. 권한 변경은 소유자와 관리자만 가능하다. **관리자**는 VIP·멤버의 정보만 수정·삭제·비밀번호 변경할 수 있으며, 소유자·관리자·본인 계정은 조회만 한다. 관리자는 소유자·관리자 등급을 부여할 수 없다. **소유자**는 모든 등급 회원 정보를 수정할 수 있고 본인 정보 수정도 가능하나, 본인 등급은 직접 낮출 수 없다. 소유자는 다른 회원을 소유자로 지정할 수 있으며, 이때 확인 모달 후 `POST /admin/api/members/:id/ownership-transfer`로 권한을 이전하고 기존 소유자는 관리자가 된다. 소유자는 항상 1명만 유지된다.
|
||||
- 관리자 페이지 접근은 `/admin/api/auth/me` 확인 후 허용한다.
|
||||
- 관리자 세션 토큰은 `ADMIN_PASSWORD` 기반 HMAC 서명으로 검증한다.
|
||||
- `/admin/api/auth/login`, `/admin/api/auth/logout`을 제외한 관리자 API 요청은 서버 미들웨어에서 세션 사용자 ID의 현재 DB 권한이 `owner` 또는 `admin`인지 다시 확인한다. 권한 변경이나 계정 삭제 이후 남아 있는 기존 관리자 쿠키는 이 단계에서 차단한다.
|
||||
|
||||
@@ -1,5 +1,58 @@
|
||||
# 업데이트 이력
|
||||
|
||||
## v1.7.1
|
||||
|
||||
- 관리자 네비게이션 추천 사이트 편집: 미저장 닫기·삭제 확인을 `AdminConfirmModal` 공통 모달로 교체.
|
||||
|
||||
## v1.7.0
|
||||
|
||||
- 관리자 멤버 권한: 관리자는 VIP·멤버 정보만 수정·삭제·비밀번호 변경 가능, 소유자·관리자·본인 계정은 조회만.
|
||||
- 소유자 멤버 관리: 다른 회원을 소유자로 지정 시 확인 모달 후 권한 이전, 기존 소유자는 관리자로 자동 변경.
|
||||
|
||||
## v1.6.9
|
||||
|
||||
- 버그 수정: 멤버 편집 저장 시 `isSaving` 때문에 등급 변경 API가 건너뛰어져 관리자로 바꿔도 멤버로 되돌아가던 문제 수정.
|
||||
|
||||
## v1.6.8
|
||||
|
||||
- 버그 수정: 메뉴 관리 페이지에서 네비게이션 API 오류 시 `null.map`으로 SSR이 깨지던 문제 수정.
|
||||
- `useAdminToast`: SSR 환경에서 `window` 접근으로 터지지 않도록 보호.
|
||||
|
||||
## v1.6.7
|
||||
|
||||
- 관리자 멤버 편집: 썸네일 업로드는 미리보기만 반영하고 **저장** 시 `PUT /admin/api/members/:id`로 적용.
|
||||
- 관리자 미디어 프로필 이미지: 연결 회원 배지를 등급(소유자·관리자·VIP·멤버)별로 표시.
|
||||
|
||||
## v1.6.6
|
||||
|
||||
- 관리자 네비게이션: 메뉴·추천 사이트 삭제 시 `confirm` 확인 추가(하위 메뉴 포함 안내).
|
||||
- 관리자 추천 사이트 편집 모달: 저장하지 않은 변경이 있을 때 취소·배경 클릭 시 닫기 확인.
|
||||
|
||||
## v1.6.5
|
||||
|
||||
- 관리자 추천 사이트 목록: 공개 Recommended와 동일한 카드 형태(아이콘·제목·설명/URL), 캐시 갱신 시각은 오른쪽 끝 유지.
|
||||
- 관리자 추천 사이트: 드래그 시 카드 위·아래 구역에 파란 삽입선·「앞에/뒤에 삽입」 안내 표시.
|
||||
- 관리자 추천 사이트 편집: URL 접두사 select에 AdminPageForm과 동일한 chevron SVG 적용.
|
||||
|
||||
## v1.6.4
|
||||
|
||||
- 네비게이션 URL: `uniare.sori.studio`처럼 프로토콜 없이 입력해도 `https://`를 자동 보정, 내부 경로 `/post` 지원 유지.
|
||||
- 관리자 추천 사이트 편집: URL 입력에 `https://`·`http://`·내부 경로 접두사 선택 추가.
|
||||
|
||||
## v1.6.3
|
||||
|
||||
- 관리자 추천 사이트: 테이블 대신 공개 Recommended와 같은 카드 목록·편집 모달 UI로 변경.
|
||||
- 관리자 추천 사이트: 목록은 썸네일·사이트·설명·캐시 갱신 시각·편집 버튼 전체 너비 레이아웃, 모달은 취소만으로 닫기.
|
||||
|
||||
## v1.6.2
|
||||
|
||||
- 추천 사이트 파비콘: 서버 캐시 컬럼(`cached_favicon_url`, `favicon_cached_at`) 추가 및 `/uploads/system/recommended-favicon-*.png` 저장.
|
||||
- 추천 사이트 파비콘: 매월 1일 03:00(KST) 자동 갱신 플러그인·`npm run navigation:refresh-recommended-favicons` 스크립트 추가.
|
||||
- 관리자 추천 사이트: 적용 아이콘 미리보기 열·썸네일 URL 옆 수동 파비콘 새로고침 버튼 추가.
|
||||
- 공개 Recommended: 수동 썸네일 > 서버 캐시 파비콘 > 외부 프록시 순으로 아이콘 표시.
|
||||
- 버그 수정: `favicon-refresh` API import 경로 오류로 Nitro 빌드 실패·개발 서버 무응답되던 문제 수정.
|
||||
- 개발 서버 스크립트: Nitro 빌드 완료 후 링크를 출력하고 Rollup/Nitro ERROR 로그를 터미널에 표시하도록 수정.
|
||||
|
||||
## v1.6.1
|
||||
|
||||
- 외부 파비콘 URL 생성: Google `s2/favicons?domain=` 대신 `faviconV2`+origin URL을 사용해 서브도메인·신규 사이트에서 기본 지구본만 나오던 문제 수정.
|
||||
|
||||
Reference in New Issue
Block a user