Files
sb-front/src/components/ChangePasswordModal.vue
T
ByungCheol 510eb0f4f8
CI / build (push) Failing after 14m54s
feat: 비밀번호 변경 UI (헤더 잠금 아이콘 + 모달)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 18:46:57 +09:00

175 lines
4.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { reactive, ref, watch, onMounted, onUnmounted } from 'vue'
import { useUiStore } from '@/stores/ui'
import { authApi } from '@/api/authApi'
const ui = useUiStore()
const form = reactive({ currentPassword: '', newPassword: '', confirmPassword: '' })
const loading = ref(false)
const error = ref(null)
watch(
() => ui.passwordOpen,
(open) => {
if (open) {
form.currentPassword = ''
form.newPassword = ''
form.confirmPassword = ''
error.value = null
}
},
)
async function handleSubmit() {
error.value = null
if (!form.currentPassword || !form.newPassword) {
error.value = '현재 비밀번호와 새 비밀번호를 입력하세요.'
return
}
if (form.newPassword.length < 8 || form.newPassword.length > 64) {
error.value = '새 비밀번호는 8~64자여야 합니다.'
return
}
if (form.newPassword !== form.confirmPassword) {
error.value = '새 비밀번호 확인이 일치하지 않습니다.'
return
}
if (form.newPassword === form.currentPassword) {
error.value = '새 비밀번호가 현재 비밀번호와 동일합니다.'
return
}
loading.value = true
try {
await authApi.changePassword({
currentPassword: form.currentPassword,
newPassword: form.newPassword,
})
ui.closePassword()
alert('비밀번호가 변경되었습니다.')
} catch (e) {
error.value = e.response?.data?.message || '비밀번호 변경에 실패했습니다.'
} finally {
loading.value = false
}
}
function onKeydown(e) {
if (e.key === 'Escape' && ui.passwordOpen) ui.closePassword()
}
onMounted(() => window.addEventListener('keydown', onKeydown))
onUnmounted(() => window.removeEventListener('keydown', onKeydown))
</script>
<template>
<Teleport to="body">
<Transition name="fade">
<div v-if="ui.passwordOpen" class="modal-backdrop" @click.self="ui.closePassword()">
<div class="modal" role="dialog" aria-modal="true" aria-label="비밀번호 변경">
<button class="close" type="button" aria-label="닫기" @click="ui.closePassword()">×</button>
<h2>비밀번호 변경</h2>
<form class="form" @submit.prevent="handleSubmit">
<input
v-model="form.currentPassword" type="password" placeholder="현재 비밀번호"
autocomplete="current-password" :disabled="loading"
/>
<input
v-model="form.newPassword" type="password" placeholder="새 비밀번호 (8~64자)"
autocomplete="new-password" :disabled="loading"
/>
<input
v-model="form.confirmPassword" type="password" placeholder="새 비밀번호 확인"
autocomplete="new-password" :disabled="loading"
/>
<button class="submit" type="submit" :disabled="loading">
{{ loading ? '변경 중...' : '변경' }}
</button>
</form>
<p v-if="error" class="msg error">{{ error }}</p>
</div>
</div>
</Transition>
</Teleport>
</template>
<style scoped>
.modal-backdrop {
position: fixed;
inset: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
padding: 1rem;
}
.modal {
position: relative;
width: 100%;
max-width: 360px;
padding: 1.75rem 1.5rem 1.5rem;
background: var(--color-background);
border: 1px solid var(--color-border);
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
}
.close {
position: absolute;
top: 0.5rem;
right: 0.6rem;
width: 2rem;
height: 2rem;
border: 0;
background: transparent;
color: var(--color-text);
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
}
h2 {
margin-bottom: 1.25rem;
font-size: 1.3rem;
text-align: center;
}
.form {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.form input {
padding: 0.6rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: 4px;
background: var(--color-background-soft);
color: var(--color-text);
}
.submit {
padding: 0.6rem 1rem;
border: 1px solid hsla(160, 100%, 37%, 1);
border-radius: 4px;
background: var(--color-background-soft);
color: hsla(160, 100%, 37%, 1);
cursor: pointer;
}
.submit:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.msg {
margin: 0.75rem 0 0;
}
.msg.error {
color: #c0392b;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.18s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>