+
diff --git a/db/migrations/028_site_settings_announcement.sql b/db/migrations/028_site_settings_announcement.sql
new file mode 100644
index 0000000..f977732
--- /dev/null
+++ b/db/migrations/028_site_settings_announcement.sql
@@ -0,0 +1,5 @@
+ALTER TABLE site_settings
+ ADD COLUMN IF NOT EXISTS announcement_enabled BOOLEAN NOT NULL DEFAULT false,
+ ADD COLUMN IF NOT EXISTS announcement_text TEXT NOT NULL DEFAULT '',
+ ADD COLUMN IF NOT EXISTS announcement_url TEXT NOT NULL DEFAULT '',
+ ADD COLUMN IF NOT EXISTS announcement_background_color TEXT NOT NULL DEFAULT '#15171a';
diff --git a/db/migrations/029_site_settings_signup_blocked_usernames.sql b/db/migrations/029_site_settings_signup_blocked_usernames.sql
new file mode 100644
index 0000000..0250aed
--- /dev/null
+++ b/db/migrations/029_site_settings_signup_blocked_usernames.sql
@@ -0,0 +1,2 @@
+ALTER TABLE site_settings
+ ADD COLUMN IF NOT EXISTS signup_blocked_usernames TEXT NOT NULL DEFAULT '["admin","master","zenn","sori","sori.studio"]';
diff --git a/docs/deploy.md b/docs/deploy.md
index 340910b..a6246c3 100644
--- a/docs/deploy.md
+++ b/docs/deploy.md
@@ -143,6 +143,8 @@ docker compose --env-file .env.production exec sori-studio-db psql -U sori_studi
docker compose --env-file .env.production exec sori-studio-db psql -U sori_studio -d sori_studio -f /docker-entrypoint-initdb.d/025_posts_status_no_private.sql
docker compose --env-file .env.production exec sori-studio-db psql -U sori_studio -d sori_studio -f /docker-entrypoint-initdb.d/026_site_settings_show_post_updated_at.sql
docker compose --env-file .env.production exec sori-studio-db psql -U sori_studio -d sori_studio -f /docker-entrypoint-initdb.d/027_site_settings_home_cover.sql
+docker compose --env-file .env.production exec sori-studio-db psql -U sori_studio -d sori_studio -f /docker-entrypoint-initdb.d/028_site_settings_announcement.sql
+docker compose --env-file .env.production exec sori-studio-db psql -U sori_studio -d sori_studio -f /docker-entrypoint-initdb.d/029_site_settings_signup_blocked_usernames.sql
```
### Docker 네트워크 충돌 대응
diff --git a/docs/map.md b/docs/map.md
index e3d180a..e8130d8 100644
--- a/docs/map.md
+++ b/docs/map.md
@@ -51,6 +51,8 @@
| 파일 | 화면 위치 |
|------|-----------|
| components/auth/AuthPasswordVisibilityToggle.vue | 로그인·회원가입 비밀번호 표시/숨김 토글(SVG, scoped 스타일·`field-name`으로 접근성 레이블 구분) |
+| components/site/SiteTopChrome.vue | 공개 레이아웃 상단 고정 영역(어나운스 바+헤더), `--site-top-chrome-height` CSS 변수 |
+| components/site/SiteAnnouncementBar.vue | 공개 사이트 상단 어나운스 배너(문구·선택 링크·배경색·닫기) |
| 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`), Authors 영역은 비공개, 푸터 `footer` 링크는 `flex-wrap`·테마 버튼 `shrink-0`, 태그 카테고리·테마 점은 `site-sidebar-nav-row` 호버 |
@@ -67,6 +69,7 @@
| 파일 | 화면 위치 |
|------|-----------|
+| components/admin/AdminSettingsNavIcon.vue | 사이트 설정 좌측 내비 항목 아이콘(`iconId`별 SVG·미구현 placeholder) |
| components/admin/AdminPostForm.vue | 관리자 글 작성/수정 폼, Ghost형 툴바(영문 상태·Publish/Update/Unpublish/Unschedule, 서버 반영 상태 기준 분기), 초안만 서버 디바운스 자동 저장·신규 임시 슬러그·발행·예약 글은 Update로만 반영, 발행 모달(중앙 배치), 좌우 설정 패널, 미리보기 emit·미저장 이탈 가드, 추천 글 토글 등 |
| components/admin/AdminPageForm.vue | 관리자 페이지 작성/수정 폼, 대표 이미지 선택 |
| components/admin/AdminMarkdownEditor.vue | 관리자 글 Markdown-first 에디터, 라이브·소스 모드 `/` 슬래시 명령·미디어 모달, 커서 블록 컨텍스트·`block-panel` emit |
@@ -127,7 +130,8 @@
| pages/admin/tags/index.vue | 태그 관리(메인 태그 드래그 정렬 자동 저장·more vert 메뉴, 일반 태그 배지 more vert 메뉴·검색/정렬, 태그 추가 버튼), 액션 피드백 토스트 |
| pages/admin/tags/new.vue | 태그 생성 |
| pages/admin/tags/[id].vue | 태그 수정 |
-| pages/admin/settings/index.vue | 사이트 설정 Ghost형 전체 화면, **POST 설정**(`showPostUpdatedAt` 수정일 표시 토글·저장), 블로그 제목·설명·기타(로고·URL·저작권), **메인 화면**(커버 이미지·오버레이 텍스트), 타임존·어나운스·Import/Export·스팸 플레이스홀더 |
+| pages/admin/settings/index.vue | 사이트 설정 Ghost형 전체 화면, **POST 설정**(`showPostUpdatedAt` 토글·읽기 모드 비활성 토글), 블로그 제목·설명·기타(로고·URL·저작권), **메인 화면**(커버 이미지·오버레이 텍스트), **어나운스 바**(사용 토글·맞춤 설정·배경색), **스팸 필터**(가입 금지 닉네임), 타임존·Import/Export 플레이스홀더 |
+| lib/signup-blocked-usernames.js | 가입 금지 닉네임 정리·매칭·안내 문구 |
| pages/admin/members/index.vue | 관리자 멤버 목록(Ghost형 테이블, 검색, 조건 필터, 멤버 추가 버튼, 닉네임+이메일, 가입일+최근 활동, IP, 댓글 수, 활동 상태 텍스트) |
| pages/admin/members/new.vue | 관리자 멤버 추가(썸네일 URL, 이름, 이메일, 레이블, 관리자 노트) |
| pages/admin/members/[id].vue | 관리자 멤버 상세/수정(회원 요약, 가입 정보, 활동 요약, 기본 정보 저장, 삭제 후 목록 이동) |
diff --git a/docs/spec.md b/docs/spec.md
index c5fe246..687d4ce 100644
--- a/docs/spec.md
+++ b/docs/spec.md
@@ -378,7 +378,7 @@ components/content/
- `GET /api/pages/:slug` - 고정 페이지 상세
- `GET /api/tags` - 태그 목록
- `GET /api/search?q=` - 통합 검색(태그 `name`·`slug`, 게시물 `title`·`excerpt`·`content` 부분 일치, 각 최대 12건, 발행 게시물만)
-- `GET /api/site-settings` - 공개 사이트 설정
+- `GET /api/site-settings` - 공개 사이트 설정(어나운스 바·홈 커버 등 포함)
- `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에 해시해 둘 때만 쓰이는 서버 비밀(긴 난문자열 권장)이다.
@@ -434,10 +434,10 @@ components/content/
- `PUT /admin/api/tags/:id` - 태그 수정
- `DELETE /admin/api/tags/:id` - 태그 삭제
- `PUT /admin/api/tags/reorder` - 메인 태그 순서 일괄 저장
-- `GET /admin/api/settings` - 사이트 설정 조회(`showPostUpdatedAt` 포함)
-- `PUT /admin/api/settings` - 사이트 설정 수정(`showPostUpdatedAt`, `homeCoverImageUrl`, `homeCoverTitle`, `homeCoverText` 포함)
+- `GET /admin/api/settings` - 사이트 설정 조회(`showPostUpdatedAt`, 어나운스 바 필드 포함)
+- `PUT /admin/api/settings` - 사이트 설정 수정(`showPostUpdatedAt`, 홈 커버, 어나운스 바, `signupBlockedUsernames` 포함). `announcementEnabled`가 true이면 `announcementText` 필수. 배경색은 `#15171a`·`#ffffff`·`#ff4f2e`만 허용.
- `POST /admin/api/settings/home-cover` - 메인 화면 커버 이미지 파일만 업로드(720px WebP, `{ homeCoverImageUrl }` 반환). `site_settings` 반영은 `PUT` 저장 시 함께 처리한다.
-- `GET /api/site-settings` - 공개 사이트 설정 조회(`showPostUpdatedAt`, 홈 커버 필드 포함)
+- `GET /api/site-settings` - 공개 사이트 설정 조회(`showPostUpdatedAt`, 홈 커버·어나운스 바 필드 포함)
- `GET /admin/api/navigation` - 네비게이션 항목 목록
- `PUT /admin/api/navigation` - 네비게이션 항목 일괄 저장
- `GET /admin/api/members` - 회원 목록(권한 코드, 최근 접속, 접속 IP, 댓글 수 포함)
@@ -561,11 +561,13 @@ components/content/
### 사이트 설정
-- 관리자 사이트 설정 UI는 `/admin/settings`에서 제공한다. Ghost Admin과 유사하게 **전체 화면**으로 표시하며, 좌측 내비와 우측 본문을 **한 덩어리로 중앙 정렬**(`max-w` 래퍼, 본문 카드 영역은 약 760px 상한)하고, 페이지 배경은 밝은 회색·본문 열은 흰색으로 구분한다. 우측 본문을 스크롤하면 현재 보이는 구역에 맞춰 좌측 메뉴 활성 배경을 갱신한다. **우측 상단 고정** 닫기 버튼과 `Escape` 키로 설정 화면을 닫으며, 브라우저 히스토리가 있으면 `뒤로 가기`, 없으면 `/admin`으로 이동한다. 타임존·어나운스 바·게시물 Import/Export·스팸 필터는 현재 **메뉴·안내 카드만** 제공하고 저장 API는 연결하지 않는다. **블로그 제목·설명** 카드는 기본적으로 사이트 이름·설명을 읽기 전용으로 보여 주고, `편집`을 눌렀을 때만 입력 필드·미리보기·저장/취소가 나타난다. 제목·설명 편집 중 `Escape`는 설정 닫기 대신 편집 취소로 동작한다.
+- 관리자 사이트 설정 UI는 `/admin/settings`에서 제공한다. Ghost Admin과 유사하게 **전체 화면**으로 표시하며, 좌측 내비와 우측 본문을 **한 덩어리로 중앙 정렬**(`max-w` 래퍼, 본문 카드 영역은 약 760px 상한)하고, 페이지 배경은 밝은 회색·본문 열은 흰색으로 구분한다. 우측 본문을 스크롤하면 현재 보이는 구역에 맞춰 좌측 메뉴 활성 배경을 갱신한다. **우측 상단 고정** 닫기 버튼과 `Escape` 키로 설정 화면을 닫으며, 브라우저 히스토리가 있으면 `뒤로 가기`, 없으면 `/admin`으로 이동한다. 타임존·게시물 Import/Export는 현재 **메뉴·안내 카드만** 제공하고 저장 API는 연결하지 않는다. **스팸 필터**는 가입 금지 닉네임을 저장한다. **블로그 제목·설명** 카드는 기본적으로 사이트 이름·설명을 읽기 전용으로 보여 주고, `편집`을 눌렀을 때만 입력 필드·미리보기·저장/취소가 나타난다. 제목·설명 편집 중 `Escape`는 설정 닫기 대신 편집 취소로 동작한다. **POST 설정**·**어나운스 바**는 읽기 모드에서도 토글 UI를 비활성화 상태로 보여 준다.
- 사이트 설정은 `site_settings` 테이블의 단일 레코드로 관리한다.
- 관리자는 사이트 이름, 설명, 사이트 URL, 로고 이미지, 저작권 문구를 수정할 수 있다.
- **메인 화면**(`home_cover_image_url`, `home_cover_title`, `home_cover_text`): 홈(`/`) 상단 720px 커버 배너. 이미지가 있을 때만 `HomeHero`를 표시하며, 제목·짧은 본문은 이미지 왼쪽 하단 그라데이션 오버레이로 겹친다. 관리자 UI에서는 커버 파일 업로드·제목·본문을 편집한 뒤 **저장** 한 번에 `PUT /admin/api/settings`로 반영한다. 파일 업로드 API는 디스크에만 올리고 URL을 돌려준다(가로 720px WebP, `/uploads/system/home-cover-YYYYMM-random.webp`).
- **POST 설정**(`show_post_updated_at`): 발행 후 본문·메타 수정이 있을 때 관리자 글 목록·공개 글 상세에 수정 시각 보조 줄을 표시할지 여부.
+- **가입 금지 닉네임**(`signup_blocked_usernames`, JSON 문자열): 회원가입·회원 프로필 닉네임 변경 시 닉네임에 목록 단어가 포함되면 거부한다(대소문자 무시, 부분 일치). 안내 문구는 `{단어}은 사용할 수 없는 단어입니다.` 형식이다. 기본값: `admin`, `master`, `zenn`, `sori`, `sori.studio`.
+- **어나운스 바**(`announcement_enabled`, `announcement_text`, `announcement_url`, `announcement_background_color`): `announcement_enabled`가 true이고 문구가 비어 있지 않으면 공개 레이아웃(`default`·`post`) 헤더 위에 전체 너비 배너를 표시한다. `announcement_url`이 있으면 문구를 링크로 감싼다(내부 `/…` 또는 `http(s)://`). 배경색은 `#15171a`·`#ffffff`·`#ff4f2e` 중 하나. 방문자가 닫으면 `localStorage` 키 `SITE_ANNOUNCEMENT_DISMISSED_AT`에 당시 `updatedAt`을 저장하고, 설정이 다시 저장되어 `updatedAt`이 바뀌면 배너가 다시 나타난다.
- 로고 이미지는 1:1 비율로 저장하며 `/admin/api/settings/logo` 업로드 시 `/uploads/system/logo-YYYYMM-random.webp`와 `/uploads/system/favicon-YYYYMM-random.png`를 고유 파일명으로 함께 생성한다. 같은 URL 덮어쓰기로 인한 브라우저·운영 정적 캐시 문제를 피하기 위해 로고 교체마다 새 URL을 저장한다.
- 공개 헤더와 오른쪽 사이드바는 공개 사이트 설정 API 값을 사용한다.
- 공개 헤더와 오른쪽 사이드바는 `logo_url`이 있으면 이미지 로고를 표시하고, 없으면 `logo_text` fallback을 쓴다. `favicon_url`은 head의 icon 링크로 연결한다.
diff --git a/docs/update.md b/docs/update.md
index 9b334df..1a9a8ae 100644
--- a/docs/update.md
+++ b/docs/update.md
@@ -1,5 +1,16 @@
# 업데이트 이력
+## v1.3.1
+
+- 스팸 필터: 가입 금지 닉네임(`signupBlockedUsernames`) 설정·저장. 기본 admin, master, zenn, sori, sori.studio. 회원가입·프로필 닉네임 변경 시 검사, `zenn은 사용할 수 없는 단어입니다.` 형식 안내. 마이그레이션 `029_site_settings_signup_blocked_usernames.sql`.
+
+## v1.3.0
+
+- 관리자 사이트 설정 좌측 내비: `AdminSettingsNavIcon`·`iconId` 슬롯 추가. 블로그 제목·설명에 type-cursor 아이콘 연결, 나머지는 placeholder 틀.
+- 관리자 POST 설정: 읽기 모드에서도 수정일 표시를 비활성화 토글 UI로 표시(켜짐/꺼짐 텍스트 제거).
+- 어나운스 바: `site_settings` 필드·마이그레이션 `028_site_settings_announcement.sql`. 공개 상단 배너(`SiteAnnouncementBar`·`SiteTopChrome`), 닫기 시 `localStorage`·설정 변경 시 재노출.
+- 관리자 설정「어나운스 바」: 사용 토글·맞춤 설정(문구·선택 링크·배경색 프리셋 검정/흰/브랜드).
+
## v1.2.9
- 홈 상단: Ghost형 헤딩·구독 폼 제거. 사이트 설정「메인 화면」에서 커버 이미지(720px)·오버레이 제목·본문 설정. `HomeHero.vue`, 마이그레이션 `027_site_settings_home_cover.sql`.
diff --git a/layouts/default.vue b/layouts/default.vue
index 3dd9f91..4e14d76 100644
--- a/layouts/default.vue
+++ b/layouts/default.vue
@@ -30,7 +30,9 @@ onBeforeUnmount(() => {