Files
sb-front/src/components/LoginModal.vue
T
ByungCheol c5e4c2dad7 feat: 웹 안내페이지(앱전용)·회원가입 제한/봇차단·카드 인식 보정
- 웹(브라우저)은 안내 페이지만, 실제 이용은 앱(Capacitor). 개발모드는 예외
- 회원가입: 약관동의, 관리자 제한 토글, 진입부터 차단, 허니팟
- 카드 알림: pending 건 계좌종류 '카드' 기본선택(현금 X), 확인실패 시 HTTP 상태 표시
- 네이티브 알림필터: '카드' 키워드 제거+광고성 표현 차단
- docs/release-2026-06-06.md 정리

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 00:16:50 +09:00

205 lines
5.0 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 { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useUiStore } from '@/stores/ui'
const auth = useAuthStore()
const ui = useUiStore()
const router = useRouter()
// 로그인 닫고 회원가입 팝업 열기
function goSignup() {
ui.openSignup()
}
const form = reactive({ loginId: '', password: '', rememberMe: true })
const loading = ref(false)
const error = ref(null)
// 팝업이 열릴 때마다 입력값 초기화 (로그인 상태 유지는 기본 켜둠)
watch(
() => ui.loginOpen,
(open) => {
if (open) {
form.loginId = ''
form.password = ''
form.rememberMe = true
error.value = null
}
},
)
async function handleLogin() {
error.value = null
if (!form.loginId || !form.password) {
error.value = '아이디와 비밀번호를 입력하세요.'
return
}
loading.value = true
try {
await auth.login(form.loginId, form.password, form.rememberMe)
const redirect = ui.redirectPath
ui.closeLogin()
if (redirect) router.push(redirect)
} catch (e) {
error.value = e.response?.data?.message || '로그인에 실패했습니다.'
} finally {
loading.value = false
}
}
function handleNaverLogin() {
alert('네이버 로그인은 준비 중입니다.')
}
// ESC 로 닫기
function onKeydown(e) {
if (e.key === 'Escape' && ui.loginOpen) ui.closeLogin()
}
onMounted(() => window.addEventListener('keydown', onKeydown))
onUnmounted(() => window.removeEventListener('keydown', onKeydown))
</script>
<template>
<Teleport to="body">
<Transition name="fade">
<div v-if="ui.loginOpen" class="modal-backdrop" @click.self="ui.closeLogin()">
<div class="modal" role="dialog" aria-modal="true" aria-label="로그인">
<button class="close" type="button" aria-label="닫기" @click="ui.closeLogin()">×</button>
<h2>로그인</h2>
<form class="form" @submit.prevent="handleLogin">
<input v-model="form.loginId" type="text" placeholder="아이디" autocomplete="username" :disabled="loading" />
<input v-model="form.password" type="password" placeholder="비밀번호" autocomplete="current-password" :disabled="loading" />
<label class="remember">
<input v-model="form.rememberMe" type="checkbox" :disabled="loading" />
로그인 상태 유지
</label>
<button class="submit" type="submit" :disabled="loading">{{ loading ? '로그인 중...' : '로그인' }}</button>
</form>
<p v-if="error" class="msg error">{{ error }}</p>
<button type="button" class="naver" :disabled="loading" @click="handleNaverLogin">
네이버로 로그인 (준비 )
</button>
<p class="links">
<template v-if="ui.signupEnabled">
계정이 없으신가요?
<a href="#" @click.prevent="goSignup">회원가입</a>
</template>
<span v-else class="signup-off">회원가입이 제한되어 있습니다.</span>
</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);
}
.remember {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.88rem;
cursor: pointer;
}
.remember input {
width: auto;
padding: 0;
cursor: pointer;
}
.submit,
.naver {
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,
.naver:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.naver {
width: 100%;
margin-top: 0.75rem;
border-color: #03c75a;
color: #03c75a;
}
.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>