v0.1.60 - 닉네임 중복 검증과 비밀번호 재설정 연결

This commit is contained in:
2026-04-23 16:47:37 +09:00
parent a78ad7e8fb
commit 8a46c507e9
5 changed files with 193 additions and 17 deletions

View File

@@ -12,9 +12,11 @@ import { fetchAdminOverview } from './lib/adminApi'
import {
clearAuthState,
fetchCurrentUser,
confirmPasswordReset,
login,
persistAuthState,
readAuthState,
requestPasswordReset,
signup,
updatePassword,
updateProfile,
@@ -67,6 +69,8 @@ const authForm = reactive({
nickname: '',
email: '',
password: '',
resetToken: '',
newPassword: '',
rememberSession: false,
})
const goalForm = reactive({
@@ -1415,6 +1419,8 @@ function resetAuthForm() {
authForm.nickname = ''
authForm.email = ''
authForm.password = ''
authForm.resetToken = ''
authForm.newPassword = ''
authForm.rememberSession = false
}
@@ -1443,6 +1449,29 @@ function openAuthDialog(mode = 'login') {
authDialogOpen.value = true
}
function openPasswordResetFromUrl() {
if (typeof window === 'undefined') {
return
}
const url = new URL(window.location.href)
if (!url.pathname.includes('reset-password')) {
return
}
const token = url.searchParams.get('token') ?? ''
if (!token) {
return
}
authForm.resetToken = token
authMode.value = 'reset-confirm'
authMessage.value = ''
authDialogOpen.value = true
}
function closeAuthDialog() {
authDialogOpen.value = false
authMessage.value = ''
@@ -1474,17 +1503,39 @@ async function submitAuthForm() {
authMessage.value = ''
try {
const result =
authMode.value === 'login'
? await login({
email: authForm.email,
password: authForm.password,
})
: await signup({
nickname: authForm.nickname,
email: authForm.email,
password: authForm.password,
})
if (authMode.value === 'reset-request') {
const result = await requestPasswordReset({
email: authForm.email,
})
authMessage.value = result.resetPreviewUrl
? `${result.message} 개발용 링크: ${result.resetPreviewUrl}`
: result.message
return
}
if (authMode.value === 'reset-confirm') {
const result = await confirmPasswordReset({
token: authForm.resetToken,
newPassword: authForm.newPassword,
})
authMode.value = 'login'
authForm.password = ''
authForm.newPassword = ''
authForm.resetToken = ''
authMessage.value = result.message
return
}
const result = authMode.value === 'login'
? await login({
email: authForm.email,
password: authForm.password,
})
: await signup({
nickname: authForm.nickname,
email: authForm.email,
password: authForm.password,
})
await applyAuthSuccess(result, authMode.value === 'login' && authForm.rememberSession)
} catch (error) {
@@ -1983,6 +2034,7 @@ onMounted(() => {
updateWindowWidth()
window.addEventListener('resize', updateWindowWidth)
window.addEventListener('keydown', handleGlobalKeydown)
openPasswordResetFromUrl()
restoreAuthSession()
})

View File

@@ -35,6 +35,58 @@ function updateField(field, event) {
value: event.target.type === 'checkbox' ? event.target.checked : event.target.value,
})
}
function getTitle(mode) {
if (mode === 'signup') {
return '회원가입'
}
if (mode === 'reset-request') {
return '비밀번호 찾기'
}
if (mode === 'reset-confirm') {
return '새 비밀번호 설정'
}
return '로그인'
}
function getDescription(mode) {
if (mode === 'signup') {
return '기록을 저장할 계정을 만들어요.'
}
if (mode === 'reset-request') {
return '가입한 이메일로 재설정 링크를 받을 수 있습니다.'
}
if (mode === 'reset-confirm') {
return '메일로 받은 링크의 토큰과 새 비밀번호를 확인합니다.'
}
return '내 플래너를 이어서 열어요.'
}
function getSubmitLabel(mode, busy) {
if (busy) {
return '처리 중...'
}
if (mode === 'signup') {
return '가입하기'
}
if (mode === 'reset-request') {
return '재설정 링크 받기'
}
if (mode === 'reset-confirm') {
return '비밀번호 재설정'
}
return '로그인하기'
}
</script>
<template>
@@ -47,10 +99,10 @@ function updateField(field, event) {
<div>
<p class="text-[10px] font-bold uppercase tracking-[0.24em] text-stone-500">10 Minute Planner</p>
<h2 class="mt-2 text-2xl font-semibold tracking-[-0.04em] text-stone-900">
{{ mode === 'login' ? '로그인' : '회원가입' }}
{{ getTitle(mode) }}
</h2>
<p class="mt-2 text-sm leading-6 text-stone-600">
{{ mode === 'login' ? '내 플래너를 이어서 열어요.' : '기록을 저장할 계정을 만들어요.' }}
{{ getDescription(mode) }}
</p>
</div>
<button
@@ -74,7 +126,7 @@ function updateField(field, event) {
/>
</div>
<div class="space-y-2">
<div v-if="mode !== 'reset-confirm'" class="space-y-2">
<label class="text-[11px] font-bold tracking-[0.16em] text-stone-600">
{{ mode === 'login' ? '이메일 또는 아이디' : '이메일' }}
</label>
@@ -87,7 +139,18 @@ function updateField(field, event) {
/>
</div>
<div class="space-y-2">
<div v-if="mode === 'reset-confirm'" class="space-y-2">
<label class="text-[11px] font-bold tracking-[0.16em] text-stone-600">재설정 토큰</label>
<input
:value="form.resetToken"
type="text"
class="w-full rounded-2xl border border-stone-300 bg-white/90 px-4 py-3 text-sm font-semibold text-stone-800 outline-none transition focus:border-stone-500"
placeholder="메일 링크의 토큰"
@input="updateField('resetToken', $event)"
/>
</div>
<div v-if="mode === 'login' || mode === 'signup'" class="space-y-2">
<label class="text-[11px] font-bold tracking-[0.16em] text-stone-600">비밀번호</label>
<input
:value="form.password"
@@ -98,6 +161,17 @@ function updateField(field, event) {
/>
</div>
<div v-if="mode === 'reset-confirm'" class="space-y-2">
<label class="text-[11px] font-bold tracking-[0.16em] text-stone-600"> 비밀번호</label>
<input
:value="form.newPassword"
type="password"
class="w-full rounded-2xl border border-stone-300 bg-white/90 px-4 py-3 text-sm font-semibold text-stone-800 outline-none transition focus:border-stone-500"
placeholder="8자 이상 입력해 주세요."
@input="updateField('newPassword', $event)"
/>
</div>
<label
v-if="mode === 'login'"
class="-mt-1 flex items-center gap-2 px-1 text-left"
@@ -123,13 +197,22 @@ function updateField(field, event) {
class="w-full rounded-full bg-stone-900 px-5 py-3 text-xs font-bold tracking-[0.18em] text-white transition hover:bg-stone-700 disabled:cursor-not-allowed disabled:bg-stone-400"
:disabled="busy"
>
{{ busy ? '처리 중...' : mode === 'login' ? '로그인하기' : '가입하기' }}
{{ getSubmitLabel(mode, busy) }}
</button>
</form>
<button
v-if="mode === 'login'"
type="button"
class="mt-4 w-full text-center text-xs font-bold tracking-[0.14em] text-stone-500 underline underline-offset-4 transition hover:text-stone-900"
@click="emit('switch-mode', 'reset-request')"
>
비밀번호를 잊으셨나요?
</button>
<div class="mt-5 flex items-center justify-center gap-2 border-t border-stone-300/70 pt-4">
<p class="text-sm font-semibold text-stone-600">
{{ mode === 'login' ? '아직 계정이 나요?' : '이미 계정이 있나요?' }}
{{ mode === 'signup' ? '이미 계정이 나요?' : '계정 화면으로 돌아갈까요?' }}
</p>
<button
type="button"

View File

@@ -107,3 +107,17 @@ export async function updatePassword(token, { currentPassword, newPassword }) {
body: { currentPassword, newPassword },
})
}
export async function requestPasswordReset({ email }) {
return request('/api/auth/password-reset/request', {
method: 'POST',
body: { email },
})
}
export async function confirmPasswordReset({ token, newPassword }) {
return request('/api/auth/password-reset/confirm', {
method: 'POST',
body: { token, newPassword },
})
}