feat: 웹 안내페이지(앱전용)·회원가입 제한/봇차단·카드 인식 보정
- 웹(브라우저)은 안내 페이지만, 실제 이용은 앱(Capacitor). 개발모드는 예외 - 회원가입: 약관동의, 관리자 제한 토글, 진입부터 차단, 허니팟 - 카드 알림: pending 건 계좌종류 '카드' 기본선택(현금 X), 확인실패 시 HTTP 상태 표시 - 네이티브 알림필터: '카드' 키워드 제거+광고성 표현 차단 - docs/release-2026-06-06.md 정리 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -86,8 +86,11 @@ onUnmounted(() => window.removeEventListener('keydown', onKeydown))
|
||||
</button>
|
||||
|
||||
<p class="links">
|
||||
계정이 없으신가요?
|
||||
<a href="#" @click.prevent="goSignup">회원가입</a>
|
||||
<template v-if="ui.signupEnabled">
|
||||
계정이 없으신가요?
|
||||
<a href="#" @click.prevent="goSignup">회원가입</a>
|
||||
</template>
|
||||
<span v-else class="signup-off">회원가입이 제한되어 있습니다.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,8 @@ import { TERMS_OF_SERVICE, PRIVACY_POLICY } from '@/constants/terms'
|
||||
const auth = useAuthStore()
|
||||
const ui = useUiStore()
|
||||
|
||||
const form = reactive({ loginId: '', password: '', passwordConfirm: '', name: '', email: '' })
|
||||
// website = 허니팟(숨김 필드, 봇이 채우면 서버에서 차단)
|
||||
const form = reactive({ loginId: '', password: '', passwordConfirm: '', name: '', email: '', website: '' })
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
@@ -30,7 +31,7 @@ watch(
|
||||
() => ui.signupOpen,
|
||||
(open) => {
|
||||
if (open) {
|
||||
Object.assign(form, { loginId: '', password: '', passwordConfirm: '', name: '', email: '' })
|
||||
Object.assign(form, { loginId: '', password: '', passwordConfirm: '', name: '', email: '', website: '' })
|
||||
agree.terms = false
|
||||
agree.privacy = false
|
||||
openDoc.value = ''
|
||||
@@ -41,6 +42,10 @@ watch(
|
||||
|
||||
async function handleSignup() {
|
||||
error.value = null
|
||||
if (!ui.signupEnabled) {
|
||||
error.value = '현재 회원가입이 제한되어 있습니다.'
|
||||
return
|
||||
}
|
||||
if (!form.loginId || !form.password || !form.name) {
|
||||
error.value = '아이디, 비밀번호, 이름은 필수입니다.'
|
||||
return
|
||||
@@ -64,6 +69,7 @@ async function handleSignup() {
|
||||
password: form.password,
|
||||
name: form.name,
|
||||
email: form.email || null,
|
||||
website: form.website, // 허니팟(정상 사용자는 빈 값)
|
||||
})
|
||||
alert('회원가입이 완료되었습니다. 로그인해 주세요.')
|
||||
ui.openLogin() // 회원가입 닫고 로그인 팝업으로 전환
|
||||
@@ -89,12 +95,20 @@ onUnmounted(() => window.removeEventListener('keydown', onKeydown))
|
||||
<button class="close" type="button" aria-label="닫기" @click="ui.closeSignup()">×</button>
|
||||
<h2>회원가입</h2>
|
||||
|
||||
<form class="form" @submit.prevent="handleSignup">
|
||||
<!-- 회원가입 제한 시: 폼 대신 안내 -->
|
||||
<div v-if="!ui.signupEnabled" class="signup-blocked">
|
||||
<p class="blk-title">🔒 현재 회원가입이 제한되어 있습니다.</p>
|
||||
<p class="blk-desc">관리자 설정에 따라 신규 회원가입을 받지 않고 있습니다.</p>
|
||||
</div>
|
||||
|
||||
<form v-else 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" />
|
||||
<!-- 허니팟: 사람에겐 숨김, 봇이 채우면 차단 -->
|
||||
<input v-model="form.website" type="text" name="website" class="hp-field" tabindex="-1" autocomplete="off" aria-hidden="true" />
|
||||
|
||||
<!-- 약관 동의 -->
|
||||
<div class="agree">
|
||||
@@ -266,6 +280,29 @@ h2 {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
/* 허니팟: 화면 밖으로 숨김(스크린리더/봇 인식 위해 display:none 대신 off-screen) */
|
||||
.hp-field {
|
||||
position: absolute !important;
|
||||
left: -9999px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.signup-blocked {
|
||||
text-align: center;
|
||||
padding: 1.5rem 0.5rem 0.5rem;
|
||||
}
|
||||
.blk-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #c0392b;
|
||||
}
|
||||
.blk-desc {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.links {
|
||||
margin-top: 1.25rem;
|
||||
text-align: center;
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<script setup>
|
||||
// PC·모바일 웹에서 노출되는 안내 페이지. 실제 이용은 앱(Capacitor)에서만.
|
||||
// 앱 다운로드 링크는 준비되면 download() / href 에 연결.
|
||||
const downloadReady = false
|
||||
function download() {
|
||||
// TODO: 앱 다운로드 링크 연결 (Play 스토어 / 직접 APK URL)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="web-only">
|
||||
<div class="card">
|
||||
<h1 class="brand">Slim Budget</h1>
|
||||
<p class="tagline">슬림하게 관리하는 가계부 · 자산 · 예산</p>
|
||||
|
||||
<div class="notice">
|
||||
<p class="lead">📱 이 서비스는 <b>앱에서</b> 이용할 수 있어요.</p>
|
||||
<p class="sub">PC·모바일 웹에서는 안내만 제공됩니다. 앱을 설치한 뒤 로그인해 주세요.</p>
|
||||
</div>
|
||||
|
||||
<button type="button" class="dl-btn" :disabled="!downloadReady" @click="download">
|
||||
{{ downloadReady ? '앱 다운로드' : '앱 다운로드 (준비 중)' }}
|
||||
</button>
|
||||
|
||||
<ul class="features">
|
||||
<li><span class="f-icon">📒</span><b>간편한 가계부</b><span>수입·지출을 빠르게 기록</span></li>
|
||||
<li><span class="f-icon">🏦</span><b>자산·부채 관리</b><span>계좌·카드·대출·투자까지</span></li>
|
||||
<li><span class="f-icon">🎯</span><b>예산과 분석</b><span>예산 대비 지출·차트</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.web-only {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
background:
|
||||
radial-gradient(120% 120% at 50% 0%, hsla(160, 100%, 37%, 0.12), transparent 60%),
|
||||
var(--color-background);
|
||||
}
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
text-align: center;
|
||||
padding: 2.4rem 1.6rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 16px;
|
||||
background: var(--color-background-soft);
|
||||
}
|
||||
.brand {
|
||||
font-size: 2rem;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
.tagline {
|
||||
margin-top: 0.4rem;
|
||||
opacity: 0.75;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.notice {
|
||||
margin: 1.6rem 0 1.2rem;
|
||||
}
|
||||
.lead {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.sub {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.86rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.dl-btn {
|
||||
width: 100%;
|
||||
padding: 0.8rem 1rem;
|
||||
border: 1px solid hsla(160, 100%, 37%, 1);
|
||||
border-radius: 10px;
|
||||
background: hsla(160, 100%, 37%, 1);
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dl-btn:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.features {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 1.8rem 0 0;
|
||||
display: grid;
|
||||
gap: 0.6rem;
|
||||
text-align: left;
|
||||
}
|
||||
.features li {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
column-gap: 0.7rem;
|
||||
align-items: center;
|
||||
padding: 0.7rem 0.9rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.features .f-icon {
|
||||
grid-row: 1 / 3;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.features b {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.features li > span:last-child {
|
||||
font-size: 0.78rem;
|
||||
opacity: 0.65;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user