태그를 관리용/일반용으로 분리하고 관리자 드래그 정렬을 추가.

댓글/회원/관리자 인증·프로필 흐름 보완과 관련 마이그레이션 및 문서를 함께 반영해 운영 동선을 안정화.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-11 18:34:23 +09:00
parent b18aca4dcc
commit cdc16c72b2
35 changed files with 1721 additions and 138 deletions

View File

@@ -6,6 +6,7 @@ definePageMeta({
const currentStep = ref(1)
const isSubmitting = ref(false)
const signupCompleted = ref(false)
const createdAdmin = ref(false)
const statusMessage = ref('')
const submitErrorMessage = ref('')
const { data: siteSettings } = await useFetch('/api/site-settings', {
@@ -14,6 +15,12 @@ const { data: siteSettings } = await useFetch('/api/site-settings', {
description: 'Configure your Self Host AFFiNE with a few simple settings.'
})
})
const { data: bootstrapStatus } = await useFetch('/api/auth/bootstrap-status', {
default: () => ({
hasUsers: true,
needsAdminSetup: false
})
})
const form = reactive({
username: '',
@@ -32,8 +39,13 @@ const errors = reactive({
const showSignupPassword = ref(false)
const showSignupPasswordConfirm = ref(false)
const isAdminBootstrapMode = computed(() => Boolean(bootstrapStatus.value?.needsAdminSetup))
const welcomeTitle = computed(() => `Welcome to ${siteSettings.value?.title || 'AFFiNE'}`)
const welcomeDescription = computed(() => siteSettings.value?.description || 'Configure your Self Host AFFiNE with a few simple settings.')
const stepTwoTitle = computed(() => (isAdminBootstrapMode.value ? '관리자 등록' : '회원 가입'))
const stepTwoDescription = computed(() => (isAdminBootstrapMode.value
? '첫 번째 사용자이므로 소유자 권한이 부여됩니다. 관리 작업 및 사용자 생성이 가능합니다.'
: '서비스 이용을 위한 회원 정보를 입력해 주세요.'))
/**
* 필드 에러 메시지를 초기화한다.
@@ -107,7 +119,7 @@ const goNextStep = async () => {
isSubmitting.value = true
try {
await $fetch('/api/auth/signup', {
const signupResult = await $fetch('/api/auth/signup', {
method: 'POST',
body: {
username: form.username.trim(),
@@ -115,11 +127,14 @@ const goNextStep = async () => {
password: form.password
}
})
createdAdmin.value = Boolean(signupResult?.isAdmin)
signupCompleted.value = true
currentStep.value = 3
statusMessage.value = '회원가입이 완료되었습니다. 잠시 후 홈으로 이동합니다.'
await navigateTo('/')
statusMessage.value = createdAdmin.value
? '관리자 등록이 완료되었습니다. 관리자 화면으로 이동합니다.'
: '회원가입이 완료되었습니다. 잠시 후 홈으로 이동합니다.'
await navigateTo(createdAdmin.value ? '/admin' : '/')
} catch (error) {
submitErrorMessage.value = error?.data?.message || '회원가입에 실패했습니다.'
} finally {
@@ -157,15 +172,15 @@ const goPreviousStep = () => {
<template v-else-if="currentStep === 2">
<p class="text-2xl font-semibold leading-tight">
회원 가입
{{ stepTwoTitle }}
</p>
<p class="mt-2 text-sm text-[#9ba3af]">
처음 생성하는 계정은 관리자 계정으로 자동 생성됩니다.
{{ stepTwoDescription }}
</p>
<form class="mt-8 space-y-5" @submit.prevent="goNextStep">
<div class="space-y-1.5">
<label class="text-xs text-[#d8dee6]">사용자명</label>
<label class="text-xs text-[#d8dee6]">{{ isAdminBootstrapMode ? '관리자 이름' : '사용자명' }}</label>
<input
v-model="form.username"
class="auth-form-input h-10 w-full rounded-[8px] border bg-transparent px-3 text-sm outline-none transition-colors"
@@ -179,7 +194,7 @@ const goPreviousStep = () => {
</div>
<div class="space-y-1.5">
<label class="text-xs text-[#d8dee6]">이메일</label>
<label class="text-xs text-[#d8dee6]">{{ isAdminBootstrapMode ? '관리자 이메일' : '이메일' }}</label>
<input
v-model="form.email"
class="auth-form-input h-10 w-full rounded-[8px] border bg-transparent px-3 text-sm outline-none transition-colors"
@@ -193,7 +208,7 @@ const goPreviousStep = () => {
</div>
<div class="space-y-1.5">
<label class="text-xs text-[#d8dee6]">비밀번호</label>
<label class="text-xs text-[#d8dee6]">{{ isAdminBootstrapMode ? '관리자 비밀번호' : '비밀번호' }}</label>
<div
class="flex items-center rounded-[8px] border transition-colors focus-within:border-[#2f6feb]"
:class="errors.password ? 'border-[#b03b43]' : 'border-[#1a212a]'"
@@ -212,7 +227,7 @@ const goPreviousStep = () => {
</div>
<div class="space-y-1.5">
<label class="text-xs text-[#d8dee6]">비밀번호 확인</label>
<label class="text-xs text-[#d8dee6]">{{ isAdminBootstrapMode ? '관리자 비밀번호 확인' : '비밀번호 확인' }}</label>
<div
class="flex items-center rounded-[8px] border transition-colors focus-within:border-[#2f6feb]"
:class="errors.passwordConfirm ? 'border-[#b03b43]' : 'border-[#1a212a]'"
@@ -247,11 +262,11 @@ const goPreviousStep = () => {
<template v-else>
<p class="text-2xl font-semibold leading-tight">
회원가입 완료
{{ createdAdmin ? '관리자 등록 완료' : '회원가입 완료' }}
</p>
<p class="mt-2 text-sm text-[#9ba3af]">
{{ form.email }} 계정으로 가입되었습니다.<br>
이제 로그인 댓글을 작성할 있습니다.
{{ form.email }} 계정으로 {{ createdAdmin ? '관리자 등록' : '가입' }}되었습니다.<br>
{{ createdAdmin ? '이제 관리자 화면에서 사이트 운영을 시작할 수 있습니다.' : '이제 로그인 후 댓글을 작성할 수 있습니다.' }}
</p>
<div class="mt-8 rounded-[10px] border border-[#1a212a] bg-[#0d1116] p-4">