v1.3.1: 어나운스 바·가입 금지 닉네임·설정 UI 개선
공개 상단 어나운스 바와 관리자 맞춤 설정을 추가하고, 스팸 필터에서 가입 금지 닉네임을 관리·검증한다. POST 설정 읽기 모드 비활성 토글과 설정 내비 아이콘 틀을 반영한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
<script setup>
|
||||
import { ANNOUNCEMENT_BACKGROUND_PRESETS } from '~/lib/announcement-bar.js'
|
||||
import {
|
||||
normalizeSignupBlockedUsernames,
|
||||
parseSignupBlockedUsernamesFromText
|
||||
} from '~/lib/signup-blocked-usernames.js'
|
||||
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
})
|
||||
@@ -9,6 +15,8 @@ const savingTitleDesc = ref(false)
|
||||
const savingMisc = ref(false)
|
||||
const savingPost = ref(false)
|
||||
const savingHomeCover = ref(false)
|
||||
const savingAnnouncement = ref(false)
|
||||
const savingSpam = ref(false)
|
||||
const uploadingLogo = ref(false)
|
||||
const uploadingHomeCover = ref(false)
|
||||
const errorMessage = ref('')
|
||||
@@ -27,6 +35,10 @@ const editMisc = ref(false)
|
||||
const editPost = ref(false)
|
||||
/** 메인 화면 커버 카드 편집 모드 여부 */
|
||||
const editHomeCover = ref(false)
|
||||
/** 어나운스 바 맞춤 설정 패널 열림 여부 */
|
||||
const customizeAnnouncement = ref(false)
|
||||
/** 스팸 필터 카드 편집 모드 여부 */
|
||||
const editSpam = ref(false)
|
||||
/** 편집 시작 시점의 제목·설명(취소 시 복원용) */
|
||||
const titleDescSnapshot = reactive({
|
||||
title: '',
|
||||
@@ -50,6 +62,17 @@ const homeCoverSnapshot = reactive({
|
||||
homeCoverTitle: '',
|
||||
homeCoverText: ''
|
||||
})
|
||||
/** 맞춤 설정 시작 시점의 어나운스 바(취소 시 복원용) */
|
||||
const announcementSnapshot = reactive({
|
||||
announcementEnabled: false,
|
||||
announcementText: '',
|
||||
announcementUrl: '',
|
||||
announcementBackgroundColor: '#15171a'
|
||||
})
|
||||
/** 편집 시작 시점의 스팸 필터(취소 시 복원용) */
|
||||
const spamSnapshot = reactive({
|
||||
signupBlockedUsernames: []
|
||||
})
|
||||
let toastTimer = null
|
||||
let scrollSpyFrame = null
|
||||
|
||||
@@ -66,7 +89,12 @@ const form = reactive({
|
||||
showPostUpdatedAt: Boolean(settings.value?.showPostUpdatedAt),
|
||||
homeCoverImageUrl: settings.value?.homeCoverImageUrl || '',
|
||||
homeCoverTitle: settings.value?.homeCoverTitle || '',
|
||||
homeCoverText: settings.value?.homeCoverText || ''
|
||||
homeCoverText: settings.value?.homeCoverText || '',
|
||||
announcementEnabled: Boolean(settings.value?.announcementEnabled),
|
||||
announcementText: settings.value?.announcementText || '',
|
||||
announcementUrl: settings.value?.announcementUrl || '',
|
||||
announcementBackgroundColor: settings.value?.announcementBackgroundColor || '#15171a',
|
||||
signupBlockedUsernames: normalizeSignupBlockedUsernames(settings.value?.signupBlockedUsernames)
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -108,20 +136,42 @@ const hasHomeCoverChanges = computed(() => editHomeCover.value && (
|
||||
))
|
||||
|
||||
/**
|
||||
* 수정일 표시 라벨
|
||||
* @returns {string} 표시 문구
|
||||
* 어나운스 바 변경 여부
|
||||
* @returns {boolean} 변경 여부
|
||||
*/
|
||||
const showPostUpdatedAtLabel = computed(() => (form.showPostUpdatedAt ? '켜짐' : '꺼짐'))
|
||||
const hasAnnouncementChanges = computed(() => customizeAnnouncement.value && (
|
||||
form.announcementEnabled !== announcementSnapshot.announcementEnabled
|
||||
|| form.announcementText !== announcementSnapshot.announcementText
|
||||
|| form.announcementUrl !== announcementSnapshot.announcementUrl
|
||||
|| form.announcementBackgroundColor !== announcementSnapshot.announcementBackgroundColor
|
||||
))
|
||||
|
||||
/**
|
||||
* 스팸 필터 변경 여부
|
||||
* @returns {boolean} 변경 여부
|
||||
*/
|
||||
const hasSpamChanges = computed(() => editSpam.value
|
||||
&& JSON.stringify(form.signupBlockedUsernames) !== JSON.stringify(spamSnapshot.signupBlockedUsernames))
|
||||
|
||||
/**
|
||||
* 가입 금지 닉네임 textarea 바인딩
|
||||
*/
|
||||
const signupBlockedUsernamesText = computed({
|
||||
get: () => form.signupBlockedUsernames.join('\n'),
|
||||
set: (value) => {
|
||||
form.signupBlockedUsernames = parseSignupBlockedUsernamesFromText(value)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 설정 화면 좌측 내비 구역 정의
|
||||
* @type {ReadonlyArray<{ heading: string, items: ReadonlyArray<{ id: string, label: string, keywords: string }> }>}
|
||||
* @type {ReadonlyArray<{ heading: string, items: ReadonlyArray<{ id: string, label: string, keywords: string, iconId?: string }> }>}
|
||||
*/
|
||||
const settingsNavGroups = [
|
||||
{
|
||||
heading: '일반',
|
||||
items: [
|
||||
{ id: 'admin-settings-section-title', label: '블로그 제목·설명', keywords: 'title description site name' },
|
||||
{ id: 'admin-settings-section-title', label: '블로그 제목·설명', keywords: 'title description site name', iconId: 'title-desc' },
|
||||
{ id: 'admin-settings-section-timezone', label: '타임존', keywords: 'timezone seoul gmt' },
|
||||
{ id: 'admin-settings-section-misc', label: '기타 설정', keywords: 'logo url copyright favicon' }
|
||||
]
|
||||
@@ -336,7 +386,12 @@ const buildSiteSettingsPayload = () => ({
|
||||
showPostUpdatedAt: Boolean(form.showPostUpdatedAt),
|
||||
homeCoverImageUrl: form.homeCoverImageUrl || '',
|
||||
homeCoverTitle: form.homeCoverTitle || '',
|
||||
homeCoverText: form.homeCoverText || ''
|
||||
homeCoverText: form.homeCoverText || '',
|
||||
announcementEnabled: Boolean(form.announcementEnabled),
|
||||
announcementText: form.announcementText || '',
|
||||
announcementUrl: form.announcementUrl || '',
|
||||
announcementBackgroundColor: form.announcementBackgroundColor || '#15171a',
|
||||
signupBlockedUsernames: normalizeSignupBlockedUsernames(form.signupBlockedUsernames)
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -595,6 +650,91 @@ const saveHomeCoverSection = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 어나운스 바 맞춤 설정 패널을 연다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const beginCustomizeAnnouncement = () => {
|
||||
announcementSnapshot.announcementEnabled = form.announcementEnabled
|
||||
announcementSnapshot.announcementText = form.announcementText
|
||||
announcementSnapshot.announcementUrl = form.announcementUrl
|
||||
announcementSnapshot.announcementBackgroundColor = form.announcementBackgroundColor
|
||||
customizeAnnouncement.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 어나운스 바 맞춤 설정을 취소한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const cancelCustomizeAnnouncement = () => {
|
||||
form.announcementEnabled = announcementSnapshot.announcementEnabled
|
||||
form.announcementText = announcementSnapshot.announcementText
|
||||
form.announcementUrl = announcementSnapshot.announcementUrl
|
||||
form.announcementBackgroundColor = announcementSnapshot.announcementBackgroundColor
|
||||
customizeAnnouncement.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 어나운스 바 설정을 저장한다.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const saveAnnouncementSection = async () => {
|
||||
if (!hasAnnouncementChanges.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const ok = await persistSiteSettings({
|
||||
successToast: '어나운스 바 설정이 저장되었습니다.',
|
||||
savingFlag: savingAnnouncement
|
||||
})
|
||||
|
||||
if (ok) {
|
||||
announcementSnapshot.announcementEnabled = form.announcementEnabled
|
||||
announcementSnapshot.announcementText = form.announcementText
|
||||
announcementSnapshot.announcementUrl = form.announcementUrl
|
||||
announcementSnapshot.announcementBackgroundColor = form.announcementBackgroundColor
|
||||
customizeAnnouncement.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 스팸 필터 편집 모드 진입
|
||||
* @returns {void}
|
||||
*/
|
||||
const beginEditSpam = () => {
|
||||
spamSnapshot.signupBlockedUsernames = [...form.signupBlockedUsernames]
|
||||
editSpam.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 스팸 필터 편집 취소
|
||||
* @returns {void}
|
||||
*/
|
||||
const cancelEditSpam = () => {
|
||||
form.signupBlockedUsernames = [...spamSnapshot.signupBlockedUsernames]
|
||||
editSpam.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 스팸 필터 저장
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const saveSpamSection = async () => {
|
||||
if (!hasSpamChanges.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const ok = await persistSiteSettings({
|
||||
successToast: '스팸 필터 설정이 저장되었습니다.',
|
||||
savingFlag: savingSpam
|
||||
})
|
||||
|
||||
if (ok) {
|
||||
spamSnapshot.signupBlockedUsernames = [...form.signupBlockedUsernames]
|
||||
editSpam.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape 키: 제목·설명 편집 중이면 취소, 아니면 설정 화면 닫기
|
||||
* @param {KeyboardEvent} event - 키보드 이벤트
|
||||
@@ -619,6 +759,21 @@ const onGlobalKeydown = (event) => {
|
||||
cancelEditPost()
|
||||
return
|
||||
}
|
||||
if (editHomeCover.value) {
|
||||
event.preventDefault()
|
||||
cancelEditHomeCover()
|
||||
return
|
||||
}
|
||||
if (customizeAnnouncement.value) {
|
||||
event.preventDefault()
|
||||
cancelCustomizeAnnouncement()
|
||||
return
|
||||
}
|
||||
if (editSpam.value) {
|
||||
event.preventDefault()
|
||||
cancelEditSpam()
|
||||
return
|
||||
}
|
||||
closeSettings()
|
||||
}
|
||||
|
||||
@@ -703,6 +858,7 @@ onBeforeUnmount(() => {
|
||||
type="button"
|
||||
@click="scrollToSection(item.id)"
|
||||
>
|
||||
<AdminSettingsNavIcon :icon-id="item.iconId" />
|
||||
<span class="min-w-0 flex-1 truncate">{{ item.label }}</span>
|
||||
</button>
|
||||
</li>
|
||||
@@ -925,7 +1081,7 @@ onBeforeUnmount(() => {
|
||||
v-if="!editMisc"
|
||||
class="admin-settings-screen__misc-readonly grid gap-6"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-4 mt-4">
|
||||
<div class="grid h-20 w-20 shrink-0 place-items-center overflow-hidden rounded-2xl border border-[#e6e8eb] bg-[#f7f8fa]">
|
||||
<img
|
||||
v-if="form.logoUrl"
|
||||
@@ -1082,11 +1238,21 @@ onBeforeUnmount(() => {
|
||||
|
||||
<div
|
||||
v-if="!editPost"
|
||||
class="admin-settings-screen__post-readonly grid gap-2 text-sm"
|
||||
class="admin-settings-screen__post-readonly border-t border-[#eceff2] pt-5 text-sm"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-4 border-t border-[#eceff2] pt-5">
|
||||
<div class="admin-settings-screen__post-toggle flex items-center justify-between gap-4">
|
||||
<span class="font-bold text-[#15171a]">수정일 표시</span>
|
||||
<span class="text-[#657080]">{{ showPostUpdatedAtLabel }}</span>
|
||||
<span class="admin-settings-screen__post-toggle-control relative inline-flex h-7 w-12 shrink-0 cursor-not-allowed items-center opacity-80">
|
||||
<input
|
||||
:checked="form.showPostUpdatedAt"
|
||||
class="peer sr-only"
|
||||
type="checkbox"
|
||||
disabled
|
||||
aria-label="수정일 표시"
|
||||
>
|
||||
<span class="absolute inset-0 rounded-full bg-[#c8ced3] transition-colors peer-checked:bg-[#15171a]" aria-hidden="true" />
|
||||
<span class="relative ml-1 size-5 rounded-full bg-white shadow transition-transform peer-checked:translate-x-5" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1257,18 +1423,126 @@ onBeforeUnmount(() => {
|
||||
|
||||
<section
|
||||
id="admin-settings-section-announcement"
|
||||
class="admin-settings-screen__card rounded-xl border border-[#e6e8eb] bg-white p-6 shadow-[0_1px_2px_rgba(15,23,42,0.04)]"
|
||||
class="admin-settings-screen__card admin-settings-screen__card--announcement relative flex flex-col gap-6 rounded-xl border border-[#d8dce1] bg-white p-5 transition-all hover:border-[#c5cbd3] hover:shadow-sm md:p-7"
|
||||
>
|
||||
<div class="admin-settings-screen__card-head mb-2">
|
||||
<h2 class="text-lg font-semibold text-[#15171a]">
|
||||
어나운스 바
|
||||
</h2>
|
||||
<p class="mt-1 text-sm leading-relaxed text-[#657080]">
|
||||
사이트 상단 공지 배너 문구와 링크를 설정합니다. (준비 중)
|
||||
</p>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="min-w-0 flex-1">
|
||||
<h2 class="text-base font-semibold text-[#15171a] md:text-lg">
|
||||
어나운스 바
|
||||
</h2>
|
||||
<p
|
||||
v-if="!customizeAnnouncement"
|
||||
class="mr-5 mt-1 text-pretty text-sm leading-relaxed text-[#657080]"
|
||||
>
|
||||
홈페이지 상단에 중요한 공지나 링크를 표시합니다. 방문자는 닫기 버튼으로 숨길 수 있으며, 설정을 바꾸면 다시 노출됩니다.
|
||||
</p>
|
||||
</div>
|
||||
<div class="-mr-1 mt-[-5px] flex shrink-0 items-center justify-start gap-2">
|
||||
<template v-if="!customizeAnnouncement">
|
||||
<button
|
||||
class="inline-flex h-7 cursor-pointer items-center justify-center rounded px-3 text-sm font-semibold whitespace-nowrap text-[#15171a] transition hover:bg-[#eceff2] hover:text-black"
|
||||
type="button"
|
||||
@click="beginCustomizeAnnouncement"
|
||||
>
|
||||
맞춤 설정
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
class="inline-flex h-7 cursor-pointer items-center justify-center rounded px-3 text-sm font-semibold whitespace-nowrap text-[#5d6673] transition hover:bg-[#eceff2] hover:text-[#15171a]"
|
||||
type="button"
|
||||
:disabled="savingAnnouncement"
|
||||
@click="cancelCustomizeAnnouncement"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex h-7 cursor-pointer items-center justify-center rounded px-3 text-sm font-semibold whitespace-nowrap text-[#15171a] transition hover:bg-[#eceff2] hover:text-black disabled:opacity-50"
|
||||
type="button"
|
||||
:disabled="savingAnnouncement || !hasAnnouncementChanges"
|
||||
@click="saveAnnouncementSection"
|
||||
>
|
||||
{{ savingAnnouncement ? '저장 중' : '저장' }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-settings-screen__placeholder mt-4 rounded-lg border border-dashed border-[#dce0e5] bg-[#f7f8fa] px-4 py-8 text-center text-sm text-[#657080]">
|
||||
이후 버전에서 노출 조건·스타일 옵션과 함께 제공합니다.
|
||||
|
||||
<label
|
||||
v-if="!customizeAnnouncement"
|
||||
class="admin-settings-screen__announcement-toggle flex items-center justify-between gap-4 border-t border-[#eceff2] pt-5 text-sm"
|
||||
>
|
||||
<span class="font-bold text-[#15171a]">사용</span>
|
||||
<span class="admin-settings-screen__post-toggle-control relative inline-flex h-7 w-12 shrink-0 cursor-not-allowed items-center opacity-80">
|
||||
<input
|
||||
:checked="form.announcementEnabled"
|
||||
class="peer sr-only"
|
||||
type="checkbox"
|
||||
disabled
|
||||
aria-label="어나운스 바 사용"
|
||||
>
|
||||
<span class="absolute inset-0 rounded-full bg-[#c8ced3] transition-colors peer-checked:bg-[#15171a]" aria-hidden="true" />
|
||||
<span class="relative ml-1 size-5 rounded-full bg-white shadow transition-transform peer-checked:translate-x-5" aria-hidden="true" />
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="admin-settings-screen__announcement-edit grid gap-6 border-t border-[#eceff2] pt-5"
|
||||
>
|
||||
<label class="admin-settings-screen__announcement-toggle flex items-center justify-between gap-4 text-sm">
|
||||
<span class="font-bold text-[#15171a]">사용</span>
|
||||
<span class="admin-settings-screen__post-toggle-control relative inline-flex h-7 w-12 shrink-0 items-center">
|
||||
<input
|
||||
v-model="form.announcementEnabled"
|
||||
class="peer sr-only"
|
||||
type="checkbox"
|
||||
aria-label="어나운스 바 사용"
|
||||
>
|
||||
<span class="absolute inset-0 rounded-full bg-[#c8ced3] transition-colors peer-checked:bg-[#15171a]" aria-hidden="true" />
|
||||
<span class="relative ml-1 size-5 rounded-full bg-white shadow transition-transform peer-checked:translate-x-5" aria-hidden="true" />
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="admin-settings-screen__field grid gap-2 text-sm">
|
||||
<span class="font-medium text-[#3f4650]">어나운스</span>
|
||||
<input
|
||||
v-model="form.announcementText"
|
||||
class="h-10 rounded-md border border-[#dce0e5] bg-white px-3 text-[#15171a] outline-none focus:border-[#15171a] focus:ring-1 focus:ring-[#15171a]"
|
||||
maxlength="200"
|
||||
placeholder="공지 문구"
|
||||
>
|
||||
</label>
|
||||
|
||||
<label class="admin-settings-screen__field grid gap-2 text-sm">
|
||||
<span class="font-medium text-[#3f4650]">링크 (선택)</span>
|
||||
<input
|
||||
v-model="form.announcementUrl"
|
||||
class="h-10 rounded-md border border-[#dce0e5] bg-white px-3 text-[#15171a] outline-none focus:border-[#15171a] focus:ring-1 focus:ring-[#15171a]"
|
||||
maxlength="500"
|
||||
placeholder="https://… 또는 /경로 (비우면 링크 없음)"
|
||||
>
|
||||
</label>
|
||||
|
||||
<div class="admin-settings-screen__field grid gap-2 text-sm">
|
||||
<span class="font-medium text-[#3f4650]">배경색</span>
|
||||
<div class="admin-settings-screen__announcement-colors flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
v-for="preset in ANNOUNCEMENT_BACKGROUND_PRESETS"
|
||||
:key="preset.id"
|
||||
class="admin-settings-screen__announcement-color relative inline-flex size-9 items-center justify-center rounded-full border transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#15171a]"
|
||||
:class="form.announcementBackgroundColor === preset.value ? 'border-[#22a06b] ring-2 ring-[#22a06b] ring-offset-2' : 'border-[#dce0e5]'"
|
||||
type="button"
|
||||
:style="{ backgroundColor: preset.value }"
|
||||
:title="preset.label"
|
||||
:aria-label="`${preset.label} 배경`"
|
||||
:aria-pressed="form.announcementBackgroundColor === preset.value"
|
||||
@click="form.announcementBackgroundColor = preset.value"
|
||||
>
|
||||
<span class="sr-only">{{ preset.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1294,18 +1568,83 @@ onBeforeUnmount(() => {
|
||||
|
||||
<section
|
||||
id="admin-settings-section-spam"
|
||||
class="admin-settings-screen__card rounded-xl border border-[#e6e8eb] bg-white p-6 shadow-[0_1px_2px_rgba(15,23,42,0.04)]"
|
||||
class="admin-settings-screen__card admin-settings-screen__card--spam relative flex flex-col gap-6 rounded-xl border border-[#d8dce1] bg-white p-5 transition-all hover:border-[#c5cbd3] hover:shadow-sm md:p-7"
|
||||
>
|
||||
<div class="admin-settings-screen__card-head mb-2">
|
||||
<h2 class="text-lg font-semibold text-[#15171a]">
|
||||
스팸 필터
|
||||
</h2>
|
||||
<p class="mt-1 text-sm leading-relaxed text-[#657080]">
|
||||
댓글·가입 등에서 스팸을 줄이기 위한 규칙을 설정합니다. (준비 중)
|
||||
</p>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="min-w-0 flex-1">
|
||||
<h2 class="text-base font-semibold text-[#15171a] md:text-lg">
|
||||
스팸 필터
|
||||
</h2>
|
||||
<p
|
||||
v-if="!editSpam"
|
||||
class="mr-5 mt-1 text-pretty text-sm leading-relaxed text-[#657080]"
|
||||
>
|
||||
회원가입 시 사용할 수 없는 닉네임을 지정합니다. 닉네임에 해당 단어가 포함되면 가입이 거부됩니다.
|
||||
</p>
|
||||
</div>
|
||||
<div class="-mr-1 mt-[-5px] flex shrink-0 items-center justify-start gap-2">
|
||||
<template v-if="!editSpam">
|
||||
<button
|
||||
class="inline-flex h-7 cursor-pointer items-center justify-center rounded px-3 text-sm font-semibold whitespace-nowrap text-[#15171a] transition hover:bg-[#eceff2] hover:text-black"
|
||||
type="button"
|
||||
@click="beginEditSpam"
|
||||
>
|
||||
편집
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
class="inline-flex h-7 cursor-pointer items-center justify-center rounded px-3 text-sm font-semibold whitespace-nowrap text-[#5d6673] transition hover:bg-[#eceff2] hover:text-[#15171a]"
|
||||
type="button"
|
||||
:disabled="savingSpam"
|
||||
@click="cancelEditSpam"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex h-7 cursor-pointer items-center justify-center rounded px-3 text-sm font-semibold whitespace-nowrap text-[#15171a] transition hover:bg-[#eceff2] hover:text-black disabled:opacity-50"
|
||||
type="button"
|
||||
:disabled="savingSpam || !hasSpamChanges"
|
||||
@click="saveSpamSection"
|
||||
>
|
||||
{{ savingSpam ? '저장 중' : '저장' }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-settings-screen__placeholder mt-4 rounded-lg border border-dashed border-[#dce0e5] bg-[#f7f8fa] px-4 py-8 text-center text-sm text-[#657080]">
|
||||
이후 버전에서 키워드·링크 제한 등 옵션을 제공합니다.
|
||||
<div
|
||||
v-if="!editSpam"
|
||||
class="admin-settings-screen__spam-readonly border-t border-[#eceff2] pt-5 text-sm"
|
||||
>
|
||||
<p class="font-medium text-[#3f4650]">
|
||||
가입 금지 닉네임
|
||||
</p>
|
||||
<ul class="mt-3 grid gap-1.5 text-[#657080]">
|
||||
<li
|
||||
v-for="term in form.signupBlockedUsernames"
|
||||
:key="term"
|
||||
class="font-mono text-[13px] text-[#15171a]"
|
||||
>
|
||||
{{ term }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="admin-settings-screen__spam-edit grid gap-2 border-t border-[#eceff2] pt-5 text-sm"
|
||||
>
|
||||
<label class="admin-settings-screen__field grid gap-2">
|
||||
<span class="font-medium text-[#3f4650]">가입 금지 닉네임</span>
|
||||
<p class="text-xs leading-relaxed text-[#657080]">
|
||||
한 줄에 하나씩 입력합니다. 대소문자는 구분하지 않으며, 닉네임에 해당 단어가 포함되면 사용할 수 없습니다.
|
||||
</p>
|
||||
<textarea
|
||||
v-model="signupBlockedUsernamesText"
|
||||
class="min-h-[10rem] resize-y rounded-md border border-[#dce0e5] bg-white px-3 py-2 font-mono text-[13px] text-[#15171a] outline-none focus:border-[#15171a] focus:ring-1 focus:ring-[#15171a]"
|
||||
rows="8"
|
||||
placeholder="admin master zenn"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<script setup>
|
||||
import {
|
||||
formatSignupBlockedUsernameMessage,
|
||||
getSignupBlockedUsernameMatch
|
||||
} from '~/lib/signup-blocked-usernames.js'
|
||||
|
||||
definePageMeta({
|
||||
layout: 'page'
|
||||
})
|
||||
@@ -12,7 +17,8 @@ const submitErrorMessage = ref('')
|
||||
const { data: siteSettings } = await useFetch('/api/site-settings', {
|
||||
default: () => ({
|
||||
title: 'AFFiNE',
|
||||
description: 'Configure your Self Host AFFiNE with a few simple settings.'
|
||||
description: 'Configure your Self Host AFFiNE with a few simple settings.',
|
||||
signupBlockedUsernames: []
|
||||
})
|
||||
})
|
||||
const { data: bootstrapStatus } = await useFetch('/api/auth/bootstrap-status', {
|
||||
@@ -78,6 +84,15 @@ const validateStepTwo = () => {
|
||||
if (!form.username.trim()) {
|
||||
errors.username = '사용자명을 입력해 주세요.'
|
||||
valid = false
|
||||
} else {
|
||||
const blockedMatch = getSignupBlockedUsernameMatch(
|
||||
form.username,
|
||||
siteSettings.value?.signupBlockedUsernames || []
|
||||
)
|
||||
if (blockedMatch) {
|
||||
errors.username = formatSignupBlockedUsernameMessage(blockedMatch)
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
|
||||
if (!form.email.trim()) {
|
||||
@@ -205,7 +220,11 @@ const goNextStep = async () => {
|
||||
: '회원가입이 완료되었습니다. 잠시 후 홈으로 이동합니다.'
|
||||
await navigateTo(createdAdmin.value ? '/admin' : '/')
|
||||
} catch (error) {
|
||||
submitErrorMessage.value = error?.data?.message || '회원가입에 실패했습니다.'
|
||||
const message = error?.data?.message || '회원가입에 실패했습니다.'
|
||||
submitErrorMessage.value = message
|
||||
if (message.includes('사용할 수 없는 단어')) {
|
||||
errors.username = message
|
||||
}
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user