Files
sb-front/src/components/SignupModal.vue
T
ByungCheol 13308a82ef feat: 가계부·게시판 프론트엔드 구현 + Slim Budget 브랜딩/모바일 대응
- 가계부: 대시보드(예산 대비 지출·분류별 파이·기간별 막대·순자산 추이),
  내역(검색·필터·태그), 계좌(은행/카드/대출/투자), 예산, 분류, 정기 거래,
  태그, 투자 포트폴리오(종목·매수/매도·평가손익)
- 게시판: 목록/상세/작성(마크다운)/댓글/태그/열람제한
- 공통: 인증(Bearer 토큰·401 처리), 모바일 반응형(드로어·아이콘 버튼),
  Slim Budget 브랜딩(로고/타이틀/푸터), 홈 대시보드 자리
- 문서: docs/FRONTEND.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 15:42:52 +09:00

175 lines
4.7 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 { useAuthStore } from '@/stores/auth'
import { useUiStore } from '@/stores/ui'
const auth = useAuthStore()
const ui = useUiStore()
const form = reactive({ loginId: '', password: '', passwordConfirm: '', name: '', email: '' })
const loading = ref(false)
const error = ref(null)
// 팝업이 열릴 때마다 입력값 초기화
watch(
() => ui.signupOpen,
(open) => {
if (open) {
Object.assign(form, { loginId: '', password: '', passwordConfirm: '', name: '', email: '' })
error.value = null
}
},
)
async function handleSignup() {
error.value = null
if (!form.loginId || !form.password || !form.name) {
error.value = '아이디, 비밀번호, 이름은 필수입니다.'
return
}
if (form.password.length < 8) {
error.value = '비밀번호는 8자 이상이어야 합니다.'
return
}
if (form.password !== form.passwordConfirm) {
error.value = '비밀번호가 일치하지 않습니다.'
return
}
loading.value = true
try {
await auth.signup({
loginId: form.loginId,
password: form.password,
name: form.name,
email: form.email || null,
})
alert('회원가입이 완료되었습니다. 로그인해 주세요.')
ui.openLogin() // 회원가입 닫고 로그인 팝업으로 전환
} catch (e) {
error.value = e.response?.data?.message || '회원가입에 실패했습니다.'
} finally {
loading.value = false
}
}
function onKeydown(e) {
if (e.key === 'Escape' && ui.signupOpen) ui.closeSignup()
}
onMounted(() => window.addEventListener('keydown', onKeydown))
onUnmounted(() => window.removeEventListener('keydown', onKeydown))
</script>
<template>
<Teleport to="body">
<Transition name="fade">
<div v-if="ui.signupOpen" class="modal-backdrop" @click.self="ui.closeSignup()">
<div class="modal" role="dialog" aria-modal="true" aria-label="회원가입">
<button class="close" type="button" aria-label="닫기" @click="ui.closeSignup()">×</button>
<h2>회원가입</h2>
<form class="form" @submit.prevent="handleSignup">
<input v-model="form.loginId" type="text" placeholder="아이디 (4~50자)" autocomplete="username" :disabled="loading" />
<input v-model="form.password" type="password" placeholder="비밀번호 (8자 이상)" autocomplete="new-password" :disabled="loading" />
<input v-model="form.passwordConfirm" type="password" placeholder="비밀번호 확인" autocomplete="new-password" :disabled="loading" />
<input v-model="form.name" type="text" placeholder="이름" :disabled="loading" />
<input v-model="form.email" type="email" placeholder="이메일 (선택)" autocomplete="email" :disabled="loading" />
<button class="submit" type="submit" :disabled="loading">{{ loading ? '처리 중...' : '회원가입' }}</button>
</form>
<p v-if="error" class="msg error">{{ error }}</p>
<p class="links">
이미 계정이 있으신가요?
<a href="#" @click.prevent="ui.openLogin()">로그인</a>
</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 var(--color-border);
border-radius: 4px;
background: var(--color-background-soft);
color: var(--color-text);
cursor: pointer;
}
.submit:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.links {
margin-top: 1.25rem;
text-align: center;
font-size: 0.9rem;
}
.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>