251212_login
This commit is contained in:
3436
bobu/app/components/PersonalInformation.vue
Normal file
3436
bobu/app/components/PersonalInformation.vue
Normal file
File diff suppressed because it is too large
Load Diff
2243
bobu/app/components/TermsofUse.vue
Normal file
2243
bobu/app/components/TermsofUse.vue
Normal file
File diff suppressed because it is too large
Load Diff
718
bobu/app/components/auth/register-form.vue
Normal file
718
bobu/app/components/auth/register-form.vue
Normal file
@@ -0,0 +1,718 @@
|
||||
<template>
|
||||
<div
|
||||
class="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center px-4 py-12"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-lg bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-sm px-6 py-8 sm:px-8"
|
||||
>
|
||||
<!-- Title -->
|
||||
<h1 class="text-center text-2xl font-bold text-gray-900 dark:text-white">
|
||||
회원가입
|
||||
</h1>
|
||||
|
||||
<!-- STEP 1: 약관 동의 -->
|
||||
<section v-if="!termsProceed" class="mt-8 space-y-6">
|
||||
<!-- 이용약관 -->
|
||||
<div
|
||||
class="border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 max-h-60 overflow-y-auto p-4"
|
||||
>
|
||||
<app-termsof-use />
|
||||
</div>
|
||||
|
||||
<!-- 이용약관 동의 -->
|
||||
<div class="space-y-3 text-sm text-gray-800 dark:text-gray-200">
|
||||
<p>
|
||||
<strong class="font-semibold text-red-500">[필수]</strong>
|
||||
위의 이용약관에 동의하십니까?
|
||||
</p>
|
||||
<div class="flex items-center gap-6">
|
||||
<label class="inline-flex items-center gap-1 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="agreedToTerms1"
|
||||
value="true"
|
||||
class="h-4 w-4 text-black focus:ring-black border-gray-300"
|
||||
/>
|
||||
<span>예</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-1 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="agreedToTerms1"
|
||||
value="false"
|
||||
class="h-4 w-4 text-black focus:ring-black border-gray-300"
|
||||
/>
|
||||
<span>아니오</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 개인정보 수집/이용 안내 -->
|
||||
<div
|
||||
class="overflow-x-auto border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900"
|
||||
>
|
||||
<table
|
||||
class="min-w-full text-xs sm:text-sm text-gray-800 dark:text-gray-200"
|
||||
>
|
||||
<thead class="bg-gray-100 dark:bg-gray-800/70">
|
||||
<tr class="text-left">
|
||||
<th class="px-3 py-2">구분</th>
|
||||
<th class="px-3 py-2">수집/이용 항목</th>
|
||||
<th class="px-3 py-2">수집/이용 목적</th>
|
||||
<th class="px-3 py-2">보유기간</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="border-t border-gray-200 dark:border-gray-700">
|
||||
<td class="px-3 py-2">필수</td>
|
||||
<td class="px-3 py-2">
|
||||
성명, 아이디, 비밀번호, 휴대폰번호, 이메일, 회원유형
|
||||
</td>
|
||||
<td class="px-3 py-2" rowspan="2">
|
||||
회원관리 및 서비스 제공 (공유 오피스·백패킹 등)
|
||||
</td>
|
||||
<td class="px-3 py-2" rowspan="2">
|
||||
탈퇴 시 즉시 파기, 관련 법령에 따른 보관 (최대 5년)
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-t border-gray-200 dark:border-gray-700">
|
||||
<td class="px-3 py-2">선택</td>
|
||||
<td class="px-3 py-2">회원 프로필 사진</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="space-y-3 text-xs sm:text-sm text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<p>
|
||||
위의 개인정보 수집·이용에 대한 동의를 거부할 권리가 있습니다. 다만,
|
||||
동의하지 않을 경우 일부 서비스 이용에 제한이 있을 수 있습니다.
|
||||
</p>
|
||||
<p>
|
||||
<strong class="font-semibold text-red-500">[필수]</strong>
|
||||
위와 같이 개인정보를 수집·이용하는 데 동의하십니까?
|
||||
</p>
|
||||
<div class="flex items-center gap-6">
|
||||
<label class="inline-flex items-center gap-1 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="agreedToTerms2"
|
||||
value="true"
|
||||
class="h-4 w-4 text-black focus:ring-black border-gray-300"
|
||||
/>
|
||||
<span>예</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-1 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="agreedToTerms2"
|
||||
value="false"
|
||||
class="h-4 w-4 text-black focus:ring-black border-gray-300"
|
||||
/>
|
||||
<span>아니오</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 다음 버튼 -->
|
||||
<button
|
||||
type="button"
|
||||
@click="termsProceed = true"
|
||||
:disabled="!(agreedToTerms1 === 'true' && agreedToTerms2 === 'true')"
|
||||
class="mt-4 flex w-full justify-center rounded-md px-4 py-3 text-sm font-semibold text-white shadow-sm transition"
|
||||
:class="{
|
||||
'bg-gray-400 cursor-not-allowed': !(
|
||||
agreedToTerms1 === 'true' && agreedToTerms2 === 'true'
|
||||
),
|
||||
'bg-black hover:bg-gray-800':
|
||||
agreedToTerms1 === 'true' && agreedToTerms2 === 'true',
|
||||
}"
|
||||
>
|
||||
다음
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<!-- STEP 2: 회원정보 입력 -->
|
||||
<section v-else class="mt-8">
|
||||
<!-- Alert -->
|
||||
<div
|
||||
v-if="reg_show_alert"
|
||||
:class="[
|
||||
'mb-4 text-center text-sm font-semibold text-white rounded-md px-3 py-2',
|
||||
reg_alert_variant,
|
||||
]"
|
||||
>
|
||||
{{ reg_alert_msg }}
|
||||
</div>
|
||||
|
||||
<vee-form
|
||||
:validation-schema="schema"
|
||||
:initial-values="userData"
|
||||
@submit="handleSubmit"
|
||||
class="space-y-4"
|
||||
>
|
||||
<!-- 이메일 -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1.5">
|
||||
<label class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
이메일
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
@click="checkEmail"
|
||||
:disabled="!abletoCheck || emailChecked"
|
||||
class="inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium border transition"
|
||||
:class="{
|
||||
'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed':
|
||||
!abletoCheck && !emailChecked,
|
||||
'bg-yellow-100 text-yellow-700 border-yellow-400':
|
||||
abletoCheck && !emailChecked,
|
||||
'bg-green-100 text-green-700 border-green-500': emailChecked,
|
||||
}"
|
||||
>
|
||||
<div v-if="emailState.checking">
|
||||
<font-awesome-icon :icon="['fas', 'spinner']" spin />
|
||||
</div>
|
||||
<svg
|
||||
v-else
|
||||
class="h-1.5 w-1.5"
|
||||
:class="{
|
||||
'fill-gray-300': !abletoCheck && !emailChecked,
|
||||
'fill-yellow-500': abletoCheck && !emailChecked,
|
||||
'fill-green-500': emailChecked,
|
||||
}"
|
||||
viewBox="0 0 6 6"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="3" cy="3" r="3" />
|
||||
</svg>
|
||||
<span>중복 확인</span>
|
||||
</button>
|
||||
</div>
|
||||
<vee-field
|
||||
v-model="emailInput"
|
||||
type="email"
|
||||
name="email"
|
||||
class="block w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-white/5 px-3 py-2 text-sm text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-black"
|
||||
placeholder="example@normadbobu.com"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
{{ emailMessage }}
|
||||
</p>
|
||||
<ErrorMessage class="mt-1 text-xs text-red-600" name="email" />
|
||||
</div>
|
||||
|
||||
<!-- 이름 -->
|
||||
<div>
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
이름
|
||||
</label>
|
||||
<vee-field
|
||||
type="text"
|
||||
name="name"
|
||||
class="block w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-white/5 px-3 py-2 text-sm text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-black"
|
||||
placeholder="이름을 입력하세요"
|
||||
/>
|
||||
<ErrorMessage class="mt-1 text-xs text-red-600" name="name" />
|
||||
</div>
|
||||
|
||||
<!-- 비밀번호 -->
|
||||
<div>
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
비밀번호
|
||||
</label>
|
||||
<vee-field
|
||||
name="password"
|
||||
:bails="false"
|
||||
v-slot="{ field, errors }"
|
||||
>
|
||||
<input
|
||||
v-bind="field"
|
||||
type="password"
|
||||
class="block w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-white/5 px-3 py-2 text-sm text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-black"
|
||||
placeholder="비밀번호를 입력하세요"
|
||||
/>
|
||||
<div
|
||||
v-for="error in errors"
|
||||
:key="error"
|
||||
class="mt-1 text-xs text-red-600"
|
||||
>
|
||||
{{ error }}
|
||||
</div>
|
||||
</vee-field>
|
||||
</div>
|
||||
|
||||
<!-- 비밀번호 확인 -->
|
||||
<div>
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
비밀번호 확인
|
||||
</label>
|
||||
<vee-field
|
||||
type="password"
|
||||
name="confirm_password"
|
||||
class="block w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-white/5 px-3 py-2 text-sm text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-black"
|
||||
placeholder="비밀번호를 다시 입력하세요"
|
||||
/>
|
||||
<ErrorMessage
|
||||
class="mt-1 text-xs text-red-600"
|
||||
name="confirm_password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 회원 유형 -->
|
||||
<div>
|
||||
<label
|
||||
class="mb-1.5 block text-sm font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
회원 유형
|
||||
</label>
|
||||
<vee-field
|
||||
as="select"
|
||||
name="membership"
|
||||
class="block w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-white/5 px-3 py-2 text-sm text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-black"
|
||||
>
|
||||
<option value="일반회원">일반회원</option>
|
||||
<option value="노마드 워커">노마드 워커</option>
|
||||
<option value="로컬 주민">로컬 주민</option>
|
||||
<option value="기업/단체">기업/단체</option>
|
||||
</vee-field>
|
||||
<ErrorMessage class="mt-1 text-xs text-red-600" name="membership" />
|
||||
</div>
|
||||
|
||||
<!-- reCAPTCHA -->
|
||||
<div ref="recaptchaWrapperRef" class="mt-2">
|
||||
<div id="recaptcha-container"></div>
|
||||
</div>
|
||||
|
||||
<!-- 휴대폰 번호 -->
|
||||
<div>
|
||||
<label
|
||||
class="flex justify-between mb-1.5 text-sm font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
<span>휴대폰 번호</span>
|
||||
<button
|
||||
v-if="startPhoneRegister"
|
||||
type="button"
|
||||
@click="resetPhonenumber"
|
||||
class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
변경하기
|
||||
</button>
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
v-model="rawPhoneNumber"
|
||||
placeholder="010-1234-5678"
|
||||
class="block w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-white/5 px-3 py-2 text-sm text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-black"
|
||||
:disabled="startPhoneRegister"
|
||||
:class="{ 'bg-gray-100 dark:bg-gray-800': startPhoneRegister }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 인증번호 입력 -->
|
||||
<div v-if="startPhoneRegister">
|
||||
<label
|
||||
class="flex justify-between mb-1.5 text-sm font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
<span>인증번호</span>
|
||||
<button
|
||||
v-if="canResend"
|
||||
type="button"
|
||||
@click="resendSms"
|
||||
class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
다시 전송하기
|
||||
</button>
|
||||
<div v-else class="text-xs text-gray-400 dark:text-gray-500">
|
||||
<font-awesome-icon :icon="['fas', 'spinner']" spin />
|
||||
</div>
|
||||
</label>
|
||||
<input
|
||||
v-model="smsCode"
|
||||
placeholder="인증번호를 입력하세요"
|
||||
class="block w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-white/5 px-3 py-2 text-sm text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-black"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 버튼 영역 -->
|
||||
<!-- 1) 이메일 미확인 -->
|
||||
<button
|
||||
v-if="!emailChecked"
|
||||
type="button"
|
||||
disabled
|
||||
class="mt-6 flex w-full justify-center rounded-md bg-gray-400 px-4 py-3 text-sm font-semibold text-white shadow-sm cursor-not-allowed"
|
||||
>
|
||||
이메일 중복 확인을 완료해주세요
|
||||
</button>
|
||||
|
||||
<!-- 2) 전화번호 인증 시작 버튼 -->
|
||||
<button
|
||||
v-if="!startPhoneRegister && emailChecked"
|
||||
type="button"
|
||||
@click="startPhoneNumberVerification"
|
||||
:disabled="!isValidPhoneNumber"
|
||||
class="mt-6 flex w-full justify-center rounded-md px-4 py-3 text-sm font-semibold text-white shadow-sm transition"
|
||||
:class="{
|
||||
'bg-gray-400 cursor-not-allowed': !isValidPhoneNumber,
|
||||
'bg-black hover:bg-gray-800': isValidPhoneNumber,
|
||||
}"
|
||||
>
|
||||
<span v-if="!isValidPhoneNumber">휴대폰 번호를 확인해주세요</span>
|
||||
<span v-else>인증번호 전송하기</span>
|
||||
</button>
|
||||
|
||||
<!-- 3) 최종 가입 버튼 -->
|
||||
<button
|
||||
v-if="startPhoneRegister"
|
||||
type="submit"
|
||||
:disabled="reg_in_submission || !emailChecked"
|
||||
class="mt-6 flex w-full justify-center rounded-md px-4 py-3 text-sm font-semibold text-white shadow-sm transition"
|
||||
:class="{
|
||||
'bg-gray-500 cursor-not-allowed':
|
||||
reg_in_submission || !emailChecked,
|
||||
'bg-black hover:bg-gray-800': !reg_in_submission && emailChecked,
|
||||
}"
|
||||
>
|
||||
<div v-if="onSmsfunction">
|
||||
<font-awesome-icon :icon="['fas', 'spinner']" spin />
|
||||
</div>
|
||||
<span v-else>가입하기</span>
|
||||
</button>
|
||||
</vee-form>
|
||||
|
||||
<!-- Loading overlay -->
|
||||
<app-loading-overlay
|
||||
:isLoading="reg_in_submission"
|
||||
:loadingMessage="loadingMessage"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, reactive, toRefs, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { ConfirmationResult } from 'firebase/auth';
|
||||
import useUserStore from '@/stores/user';
|
||||
import { checkEmailDuplicate } from '@/utils/firebaseUtils';
|
||||
import AppTermsofUse from '~/components/TermsofUse.vue';
|
||||
import AppLoadingOverlay from '~/components/LoadingOverlay.vue';
|
||||
import {
|
||||
auth,
|
||||
phoneNumberAuth,
|
||||
createRecaptchaVerifier,
|
||||
} from '@/utils/firebaseUtils';
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const { signInWithPhoneNumber } = phoneNumberAuth;
|
||||
const emits = defineEmits(['register-success']);
|
||||
|
||||
/* ---------- STEP control ---------- */
|
||||
const termsProceed = ref(false);
|
||||
const agreedToTerms1 = ref<'true' | 'false' | ''>('');
|
||||
const agreedToTerms2 = ref<'true' | 'false' | ''>('');
|
||||
|
||||
/* ---------- VeeValidate schema ---------- */
|
||||
const schema = {
|
||||
name: 'required|min:2|max:10',
|
||||
email: 'required|min:2|max:100|email',
|
||||
password: 'required|min:9|max:100|excluded:password',
|
||||
confirm_password: 'passwords_mismatch:@password',
|
||||
membership: 'required|membership_excluded',
|
||||
};
|
||||
|
||||
interface RegisterValues {
|
||||
email: string;
|
||||
password: string;
|
||||
confirm_password: string;
|
||||
name: string;
|
||||
membership: string;
|
||||
isActive: boolean;
|
||||
profile_img: string;
|
||||
created: any;
|
||||
uid: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
const userData = ref({
|
||||
membership: '일반회원',
|
||||
});
|
||||
|
||||
/* ---------- Email duplication check ---------- */
|
||||
const loadingMessage = 'Uploading! 잠시만 기다려주세요...';
|
||||
const emailState = reactive({
|
||||
emailInput: '',
|
||||
abletoCheck: false,
|
||||
emailChecked: false,
|
||||
emailMessage: '',
|
||||
checking: false,
|
||||
});
|
||||
|
||||
const { emailInput, abletoCheck, emailChecked, emailMessage } =
|
||||
toRefs(emailState);
|
||||
|
||||
const checkEmail = async () => {
|
||||
emailState.checking = true;
|
||||
if (!emailState.abletoCheck) {
|
||||
emailState.checking = false;
|
||||
return;
|
||||
}
|
||||
const result = await checkEmailDuplicate(emailState.emailInput);
|
||||
switch (result.status) {
|
||||
case 'exists':
|
||||
emailState.emailMessage = '이미 존재하는 이메일입니다.';
|
||||
emailState.checking = false;
|
||||
break;
|
||||
case 'available':
|
||||
emailState.emailMessage = '가입 가능한 이메일입니다.';
|
||||
emailState.emailChecked = true;
|
||||
emailState.checking = false;
|
||||
break;
|
||||
case 'error':
|
||||
default:
|
||||
emailState.abletoCheck = false;
|
||||
emailState.emailMessage = result.message;
|
||||
emailState.checking = false;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => emailState.emailInput,
|
||||
(newValue) => {
|
||||
emailState.abletoCheck = false;
|
||||
emailState.emailChecked = false;
|
||||
emailState.emailMessage = '';
|
||||
|
||||
const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
|
||||
emailState.abletoCheck = emailRegex.test(newValue);
|
||||
}
|
||||
);
|
||||
|
||||
/* ---------- Phone verification ---------- */
|
||||
const firebaseAuth = auth;
|
||||
const rawPhoneNumber = ref('');
|
||||
const phoneNumber = ref('');
|
||||
const smsCode = ref('');
|
||||
const startPhoneRegister = ref(false);
|
||||
const phoneVerified = ref(false);
|
||||
const onSmsfunction = ref(false);
|
||||
const canResend = ref(true);
|
||||
const cooldownTime = ref(5);
|
||||
const recaptchaWrapperRef = ref<HTMLElement | null>(null);
|
||||
|
||||
const isValidPhoneNumber = computed(() => {
|
||||
if (typeof rawPhoneNumber.value !== 'string') return false;
|
||||
const cleaned = rawPhoneNumber.value.replace(/-/g, '');
|
||||
return (
|
||||
(cleaned.startsWith('010') || cleaned.startsWith('070')) &&
|
||||
cleaned.length === 11
|
||||
);
|
||||
});
|
||||
|
||||
const resetPhonenumber = () => {
|
||||
rawPhoneNumber.value = '';
|
||||
startPhoneRegister.value = false;
|
||||
recaptchaVerifier = null;
|
||||
confirmationResult = null;
|
||||
};
|
||||
|
||||
const processedPhoneNumber = computed(() => {
|
||||
if (typeof rawPhoneNumber.value !== 'string') return '';
|
||||
let cleaned = rawPhoneNumber.value.replace(/-/g, '');
|
||||
if (cleaned.startsWith('0')) {
|
||||
cleaned = '+82' + cleaned.substring(1);
|
||||
}
|
||||
return cleaned;
|
||||
});
|
||||
|
||||
let recaptchaVerifier: any | null = null;
|
||||
let confirmationResult: ConfirmationResult | null = null;
|
||||
|
||||
const startPhoneNumberVerification = async () => {
|
||||
try {
|
||||
phoneNumber.value = processedPhoneNumber.value;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
try {
|
||||
startPhoneRegister.value = true;
|
||||
|
||||
if (recaptchaVerifier) {
|
||||
try {
|
||||
await recaptchaVerifier.verify();
|
||||
} catch (recaptchaError) {
|
||||
console.error(
|
||||
'Previous reCAPTCHA verification failed:',
|
||||
recaptchaError
|
||||
);
|
||||
return;
|
||||
}
|
||||
recaptchaVerifier.clear();
|
||||
}
|
||||
|
||||
if (recaptchaWrapperRef.value) {
|
||||
recaptchaWrapperRef.value.innerHTML =
|
||||
'<div id="recaptcha-container"></div>';
|
||||
} else {
|
||||
console.error('recaptchaWrapperRef is null');
|
||||
return;
|
||||
}
|
||||
|
||||
recaptchaVerifier = createRecaptchaVerifier('recaptcha-container');
|
||||
|
||||
try {
|
||||
await recaptchaVerifier.verify();
|
||||
} catch (recaptchaError) {
|
||||
console.error('Error during reCAPTCHA verification:', recaptchaError);
|
||||
throw recaptchaError;
|
||||
}
|
||||
|
||||
confirmationResult = await signInWithPhoneNumber(
|
||||
firebaseAuth,
|
||||
phoneNumber.value,
|
||||
recaptchaVerifier
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error during phone number verification:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const resendSms = async () => {
|
||||
if (!confirmationResult) {
|
||||
console.error('사이트에 문제가 있습니다, 관리자에게 문의하십시오.');
|
||||
onSmsfunction.value = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
canResend.value = false;
|
||||
|
||||
if (recaptchaVerifier) {
|
||||
recaptchaVerifier.clear();
|
||||
if (recaptchaWrapperRef.value) {
|
||||
recaptchaWrapperRef.value.innerHTML =
|
||||
'<div id="recaptcha-container"></div>';
|
||||
} else {
|
||||
console.error('recaptchaWrapperRef is null');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
recaptchaVerifier = createRecaptchaVerifier('recaptcha-container');
|
||||
|
||||
confirmationResult = await signInWithPhoneNumber(
|
||||
firebaseAuth,
|
||||
phoneNumber.value,
|
||||
recaptchaVerifier
|
||||
);
|
||||
|
||||
let remaining = cooldownTime.value;
|
||||
const timer = setInterval(() => {
|
||||
remaining--;
|
||||
if (remaining <= 0) {
|
||||
clearInterval(timer);
|
||||
canResend.value = true;
|
||||
}
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error('Error during SMS resend:', error);
|
||||
canResend.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const verifySmsCode = async (): Promise<string | null> => {
|
||||
try {
|
||||
onSmsfunction.value = true;
|
||||
if (confirmationResult) {
|
||||
const result = await confirmationResult.confirm(smsCode.value);
|
||||
phoneVerified.value = true;
|
||||
await firebaseAuth.signOut();
|
||||
onSmsfunction.value = false;
|
||||
return result.user?.uid || null;
|
||||
} else {
|
||||
console.error('confirmationResult is null.');
|
||||
onSmsfunction.value = false;
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during SMS code verification:', error);
|
||||
onSmsfunction.value = false;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/* ---------- Registration submit ---------- */
|
||||
const reg_in_submission = ref(false);
|
||||
const reg_show_alert = ref(false);
|
||||
const reg_alert_variant = ref('bg-blue-500');
|
||||
const reg_alert_msg = ref('계정 생성 중입니다. 잠시만 기다려주세요!');
|
||||
|
||||
const handleSubmit = async (_event: Event, values: Record<string, any>) => {
|
||||
reg_show_alert.value = true;
|
||||
reg_in_submission.value = true;
|
||||
reg_alert_variant.value = 'bg-blue-500';
|
||||
reg_alert_msg.value = '계정 생성 중입니다. 잠시만 기다려주세요!';
|
||||
|
||||
try {
|
||||
const userUID = await verifySmsCode();
|
||||
|
||||
try {
|
||||
if (userUID) {
|
||||
await visitorRegister({
|
||||
...values.controlledValues,
|
||||
uid: userUID,
|
||||
phone: phoneNumber.value,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
reg_in_submission.value = false;
|
||||
reg_alert_variant.value = 'bg-red-500';
|
||||
reg_alert_msg.value = '계정 생성 실패!';
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('error Creating ID', error);
|
||||
reg_in_submission.value = false;
|
||||
reg_alert_variant.value = 'bg-red-500';
|
||||
reg_alert_msg.value = '계정 생성 실패!';
|
||||
return;
|
||||
}
|
||||
|
||||
reg_in_submission.value = false;
|
||||
reg_alert_variant.value = 'bg-green-500';
|
||||
reg_alert_msg.value = '성공적으로 계정이 생성되었습니다!';
|
||||
|
||||
emits('register-success');
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
async function visitorRegister(values: RegisterValues) {
|
||||
try {
|
||||
const { confirm_password, ...dataToSubmit } = values;
|
||||
await userStore.visitorRegister(dataToSubmit);
|
||||
} catch (error) {
|
||||
console.log('visitorRegisterError', error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Chrome, Safari, Edge, Opera: remove number input arrows if any */
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
243
bobu/app/components/auth/user-login.vue
Normal file
243
bobu/app/components/auth/user-login.vue
Normal file
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<div
|
||||
class="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center px-4 py-12"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-md bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-sm px-6 py-8 sm:px-8"
|
||||
>
|
||||
<!-- Title -->
|
||||
<h1 class="text-center text-2xl font-bold text-gray-900 dark:text-white">
|
||||
로그인
|
||||
</h1>
|
||||
|
||||
<!-- Social logins -->
|
||||
<div class="mt-8 space-y-3">
|
||||
<!-- Google -->
|
||||
<button
|
||||
type="button"
|
||||
@click="handleSocialLogin('google')"
|
||||
class="w-full flex items-center gap-3 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2.5 text-sm font-medium text-gray-800 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-800 transition"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center h-6 w-6 rounded-full bg-white"
|
||||
>
|
||||
<!-- Placeholder icon: replace with real Google logo later -->
|
||||
<font-awesome-icon
|
||||
:icon="['fab', 'google']"
|
||||
class="text-red-500 text-lg"
|
||||
/>
|
||||
</div>
|
||||
<span class="flex-1 text-center">Google로 로그인하기</span>
|
||||
</button>
|
||||
|
||||
<!-- Kakao -->
|
||||
<button
|
||||
type="button"
|
||||
@click="handleSocialLogin('kakao')"
|
||||
class="w-full flex items-center gap-3 rounded-md border border-yellow-400 bg-[#FEE500] px-3 py-2.5 text-sm font-medium text-gray-900 hover:bg-[#FDE64B] transition"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center h-6 w-6 rounded-full bg-black/5"
|
||||
>
|
||||
<!-- Placeholder icon: Kakao logo image or icon later -->
|
||||
<img
|
||||
v-if="socialImages.kakao"
|
||||
:src="socialImages.kakao"
|
||||
alt="Kakao"
|
||||
class="h-5 w-5 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<span class="flex-1 text-center">Kakao로 로그인하기</span>
|
||||
</button>
|
||||
|
||||
<!-- Naver -->
|
||||
<button
|
||||
type="button"
|
||||
@click="handleSocialLogin('naver')"
|
||||
class="w-full flex items-center gap-3 rounded-md border border-green-600 bg-white dark:bg-gray-900 px-3 py-2.5 text-sm font-medium text-gray-800 dark:text-gray-100 hover:bg-green-50 dark:hover:bg-gray-800 transition"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center h-6 w-6 rounded-full bg-green-600"
|
||||
>
|
||||
<img
|
||||
v-if="socialImages.naver"
|
||||
:src="socialImages.naver"
|
||||
alt="Naver"
|
||||
class="h-4 w-4 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<span class="flex-1 text-center">Naver로 로그인하기</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- separator -->
|
||||
<div class="mt-6 flex items-center">
|
||||
<div class="h-px flex-1 bg-gray-200 dark:bg-gray-700"></div>
|
||||
<span class="mx-3 text-xs text-gray-400 dark:text-gray-500">또는</span>
|
||||
<div class="h-px flex-1 bg-gray-200 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
|
||||
<!-- ID / Email + Password form -->
|
||||
<form class="mt-6 space-y-4" @submit.prevent="handleSubmit">
|
||||
<!-- 아이디 또는 이메일 -->
|
||||
<div>
|
||||
<label
|
||||
for="email"
|
||||
class="block text-sm font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
아이디 또는 이메일
|
||||
</label>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
v-model="email"
|
||||
type="text"
|
||||
name="email"
|
||||
id="email"
|
||||
autocomplete="email"
|
||||
required
|
||||
class="block w-full rounded-md bg-white dark:bg-white/5 px-3 py-2 text-sm text-gray-900 dark:text-white outline-1 -outline-offset-1 outline-gray-300 dark:outline-white/10 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:-outline-offset-2 focus:outline-black"
|
||||
placeholder="example@normadbobu.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 비밀번호 -->
|
||||
<div>
|
||||
<label
|
||||
for="password"
|
||||
class="block text-sm font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
비밀번호
|
||||
</label>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
v-model="password"
|
||||
type="password"
|
||||
name="password"
|
||||
id="password"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
class="block w-full rounded-md bg-white dark:bg-white/5 px-3 py-2 text-sm text-gray-900 dark:text-white outline-1 -outline-offset-1 outline-gray-300 dark:outline-white/10 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:-outline-offset-2 focus:outline-black"
|
||||
placeholder="●●●●●●●●"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 로그인 상태 유지 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<label
|
||||
class="flex items-center gap-2 text-xs sm:text-sm text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<input
|
||||
v-model="rememberMe"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-black focus:ring-black"
|
||||
/>
|
||||
<span>기억하기</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Alert -->
|
||||
<div
|
||||
v-if="login_show_alert"
|
||||
:class="[
|
||||
'text-white text-center text-sm font-semibold rounded-md p-3',
|
||||
login_alert_variant,
|
||||
]"
|
||||
>
|
||||
{{ login_alert_msg }}
|
||||
</div>
|
||||
|
||||
<!-- 로그인 button -->
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="login_in_submission"
|
||||
class="mt-2 flex w-full justify-center rounded-md bg-black px-3 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-black disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
>
|
||||
로그인
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- 아래 링크들 -->
|
||||
<div
|
||||
class="mt-4 flex items-center justify-between text-xs sm:text-sm text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<NuxtLink to="/register" class="hover:underline"> 회원가입 </NuxtLink>
|
||||
<NuxtLink to="/find-account" class="hover:underline">
|
||||
아이디 · 비밀번호 찾기
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- small separator -->
|
||||
<p class="mt-6 text-center text-xs text-gray-400 dark:text-gray-500">
|
||||
또는
|
||||
</p>
|
||||
|
||||
<!-- 비회원 예약 및 주문 조회 -->
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 w-full rounded-md bg-slate-400 px-3 py-2.5 text-sm font-semibold text-white hover:bg-slate-500 transition"
|
||||
>
|
||||
비회원 예약 및 주문 조회
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from '#imports';
|
||||
import useUserStore from '@/stores/user';
|
||||
import { SOCIAL_IMAGES } from '@/data/assets';
|
||||
import type { LoginValues } from '@/types';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
|
||||
const email = ref('');
|
||||
const password = ref('');
|
||||
const rememberMe = ref(false); // currently not used in logic, placeholder for future
|
||||
|
||||
const login_in_submission = ref(false);
|
||||
const login_show_alert = ref(false);
|
||||
const login_alert_variant = ref('bg-blue-500');
|
||||
const login_alert_msg = ref('로그인 중입니다. 잠시만 기다려주세요!');
|
||||
|
||||
const socialImages = SOCIAL_IMAGES;
|
||||
|
||||
// placeholder handler for future social login wiring
|
||||
const handleSocialLogin = (provider: 'google' | 'kakao' | 'naver') => {
|
||||
console.log(`Social login clicked: ${provider}`);
|
||||
// TODO: implement real social login (Firebase Auth or other)
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const values: LoginValues = {
|
||||
email: email.value,
|
||||
password: password.value,
|
||||
};
|
||||
|
||||
login_in_submission.value = true;
|
||||
login_show_alert.value = true;
|
||||
login_alert_variant.value = 'bg-blue-500';
|
||||
login_alert_msg.value = '로그인 중입니다. 잠시만 기다려주세요!';
|
||||
|
||||
// 도메인 자동 보정: @normadbobu.com
|
||||
if (!values.email.includes('@')) {
|
||||
values.email += '@normadbobu.com';
|
||||
}
|
||||
|
||||
try {
|
||||
await userStore.authenticate(values);
|
||||
} catch (error) {
|
||||
login_in_submission.value = false;
|
||||
login_alert_variant.value = 'bg-red-500';
|
||||
login_alert_msg.value = '아이디 또는 비밀번호가 일치하지 않습니다.';
|
||||
return;
|
||||
}
|
||||
|
||||
login_alert_variant.value = 'bg-green-500';
|
||||
login_alert_msg.value = '로그인에 성공하였습니다!';
|
||||
router.push('/');
|
||||
};
|
||||
</script>
|
||||
@@ -4,11 +4,11 @@
|
||||
<NuxtLink to="/wadiz/manage">TO manage</NuxtLink>
|
||||
</div>
|
||||
<div v-else>
|
||||
<AdminLogin />
|
||||
<UserLogin />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import AdminLogin from '~/components/auth/admin-login.vue';
|
||||
import UserLogin from '~/components/auth/user-login.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const userLoggedIn = computed(() => userStore.userLoggedIn);
|
||||
|
||||
20
bobu/app/pages/register.vue
Normal file
20
bobu/app/pages/register.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div v-if="userLoggedIn">
|
||||
<button @click="userStore.signOut">로그아웃</button>
|
||||
<NuxtLink to="/wadiz/manage">TO manage</NuxtLink>
|
||||
</div>
|
||||
<div v-else>
|
||||
<RegisterForm />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import RegisterForm from '~/components/auth/register-form.vue';
|
||||
const userStore = useUserStore();
|
||||
const userLoggedIn = computed(() => userStore.userLoggedIn);
|
||||
|
||||
onMounted(() => {
|
||||
if (userLoggedIn.value) {
|
||||
navigateTo('/');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user