Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 24031d93a9 | |||
| 6c0f1778c8 |
@@ -30,9 +30,12 @@ export function useAuth() {
|
||||
await api.post('/auth/reset-password', { token, password })
|
||||
}
|
||||
|
||||
// 로그인
|
||||
async function login(payload: LoginPayload): Promise<AuthUser> {
|
||||
return (await api.post('/auth/login', payload)) as unknown as AuthUser
|
||||
// 로그인 — silent 옵션 시 전역 에러 토스트를 띄우지 않고 호출측이 직접 처리
|
||||
async function login(
|
||||
payload: LoginPayload,
|
||||
opts?: { silent?: boolean },
|
||||
): Promise<AuthUser> {
|
||||
return (await api.post('/auth/login', payload, opts)) as unknown as AuthUser
|
||||
}
|
||||
|
||||
// 로그아웃 (쿠키 제거)
|
||||
|
||||
@@ -4,10 +4,12 @@ import { computed, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import RelayMark from '@/components/RelayMark.vue'
|
||||
import { useAuthStore } from '@/stores/auth.store'
|
||||
import { useDialog } from '@/composables/useDialog'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const dialog = useDialog()
|
||||
|
||||
// 폼 상태
|
||||
const email = ref('')
|
||||
@@ -16,12 +18,6 @@ const showPassword = ref(false)
|
||||
const keepLogin = ref(true)
|
||||
const submitting = ref(false)
|
||||
|
||||
// 미인증 계정 로그인 시도 시(서버 403) 인라인 재발송 안내 노출
|
||||
const needsVerify = ref(false)
|
||||
const pendingEmail = ref('')
|
||||
const resending = ref(false)
|
||||
const resendDone = ref(false)
|
||||
|
||||
// 상단 안내 배너 — 이메일 인증(/login?verified=1|0) 또는 비밀번호 재설정 완료(/login?reset=1)
|
||||
const verifyBanner = computed<{ ok: boolean; text: string } | null>(() => {
|
||||
if (route.query.reset === '1')
|
||||
@@ -40,41 +36,42 @@ const verifyBanner = computed<{ ok: boolean; text: string } | null>(() => {
|
||||
async function onLogin() {
|
||||
if (submitting.value) return
|
||||
submitting.value = true
|
||||
needsVerify.value = false
|
||||
try {
|
||||
await authStore.login({
|
||||
email: email.value.trim(),
|
||||
password: password.value,
|
||||
keepLogin: keepLogin.value,
|
||||
})
|
||||
// silent: 미인증(403)은 토스트 없이 화면 전환으로 처리하므로 전역 에러 알림을 끈다
|
||||
await authStore.login(
|
||||
{
|
||||
email: email.value.trim(),
|
||||
password: password.value,
|
||||
keepLogin: keepLogin.value,
|
||||
},
|
||||
{ silent: true },
|
||||
)
|
||||
await router.push('/projects')
|
||||
} catch (err) {
|
||||
// 미인증 계정은 서버가 403 으로 응답 → 인라인 재발송 안내 노출(메시지 토스트는 인터셉터가 처리)
|
||||
const status = (err as { response?: { status?: number } })?.response?.status
|
||||
if (status === 403) {
|
||||
pendingEmail.value = email.value.trim()
|
||||
resendDone.value = false
|
||||
needsVerify.value = true
|
||||
const res = (
|
||||
err as { response?: { status?: number; data?: { error?: { message?: string } } } }
|
||||
)?.response
|
||||
// 미인증 계정(서버 403) → 인증 메일을 즉시 발송하고 '발송 완료' 화면으로 조용히 이동
|
||||
if (res?.status === 403) {
|
||||
const addr = email.value.trim()
|
||||
try {
|
||||
await authStore.resendVerification(addr)
|
||||
} catch {
|
||||
// 재발송 실패는 무시(화면 전환 우선)
|
||||
}
|
||||
await router.push({ path: '/signup', query: { sent: addr } })
|
||||
return
|
||||
}
|
||||
// 그 외(자격 불일치 등) — 서버 메시지를 직접 안내
|
||||
void dialog.alert(
|
||||
res?.data?.error?.message ?? '이메일 또는 비밀번호가 올바르지 않습니다.',
|
||||
{ variant: 'danger' },
|
||||
)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 인증 메일 재발송 — 미인증 로그인 안내에서 호출(존재 여부는 서버가 노출하지 않음)
|
||||
async function resendVerify() {
|
||||
if (resending.value || !pendingEmail.value) return
|
||||
resending.value = true
|
||||
try {
|
||||
await authStore.resendVerification(pendingEmail.value)
|
||||
resendDone.value = true
|
||||
} catch {
|
||||
// 에러 메시지는 API 인터셉터에서 전역 처리됨
|
||||
} finally {
|
||||
resending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 소셜 로그인 — 백엔드 OAuth 엔드포인트로 전체 페이지 이동(쿠키 기반 세션이라 새 창 X)
|
||||
const apiBase = import.meta.env.VITE_API_BASE_URL || '/api'
|
||||
function goSocial(provider: 'google' | 'kakao') {
|
||||
@@ -183,32 +180,6 @@ const points = [
|
||||
{{ verifyBanner.text }}
|
||||
</div>
|
||||
|
||||
<!-- 미인증 계정 로그인 시도 — 인증 메일 재발송 안내 -->
|
||||
<div
|
||||
v-if="needsVerify"
|
||||
class="verify-banner err resend-banner"
|
||||
role="status"
|
||||
>
|
||||
<div class="rb-msg">
|
||||
이메일 인증이 완료되지 않았습니다. 받은 인증 메일의 링크로 가입을 완료해 주세요.
|
||||
</div>
|
||||
<button
|
||||
v-if="!resendDone"
|
||||
class="rb-btn"
|
||||
type="button"
|
||||
:disabled="resending"
|
||||
@click="resendVerify"
|
||||
>
|
||||
{{ resending ? '보내는 중…' : '인증 메일 다시 보내기' }}
|
||||
</button>
|
||||
<div
|
||||
v-else
|
||||
class="rb-done"
|
||||
>
|
||||
인증 메일을 다시 보냈습니다. 메일함을 확인해 주세요.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 소셜 로그인 -->
|
||||
<div class="socials">
|
||||
<button
|
||||
@@ -571,34 +542,6 @@ const points = [
|
||||
height: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* 미인증 재발송 안내 — 메시지 + 액션 버튼(세로 배치) */
|
||||
.resend-banner {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.resend-banner .rb-btn {
|
||||
border: 1px solid var(--red-border);
|
||||
background: #fff;
|
||||
color: var(--red);
|
||||
font-family: inherit;
|
||||
font-size: 0.781rem;
|
||||
font-weight: 700;
|
||||
padding: 0.4375rem 0.75rem;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
.resend-banner .rb-btn:hover:not(:disabled) {
|
||||
background: #fff5f5;
|
||||
}
|
||||
.resend-banner .rb-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
.resend-banner .rb-done {
|
||||
font-weight: 600;
|
||||
color: #1b7a3d;
|
||||
}
|
||||
.socials {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
// 회원가입 — 가입 폼 → 인증 메일 발송 안내. 인증 메일의 링크를 눌러야 가입이 완료된다.
|
||||
import { computed, ref } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
import RelayMark from '@/components/RelayMark.vue'
|
||||
import { useAuthStore } from '@/stores/auth.store'
|
||||
import { useDialog } from '@/composables/useDialog'
|
||||
@@ -27,6 +27,14 @@ const resending = ref(false)
|
||||
const resendDone = ref(false)
|
||||
const displayEmail = computed(() => sentEmail.value || email.value.trim() || 'name@acme.co')
|
||||
|
||||
// 로그인 화면에서 미인증으로 넘어온 경우(?sent=이메일) — 메일은 이미 발송됐으므로 바로 '발송 완료' 화면 표시
|
||||
const route = useRoute()
|
||||
const sentParam = route.query.sent
|
||||
if (typeof sentParam === 'string' && sentParam.trim()) {
|
||||
sentEmail.value = sentParam.trim()
|
||||
step.value = 'verify'
|
||||
}
|
||||
|
||||
// 가입 처리 — 검증 후 API 호출, 성공 시 인증 메일 안내 화면으로 전환
|
||||
async function submit() {
|
||||
if (submitting.value) return
|
||||
|
||||
@@ -24,9 +24,12 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
fetchMe,
|
||||
} = useAuth()
|
||||
|
||||
// 로그인
|
||||
async function login(payload: LoginPayload): Promise<void> {
|
||||
user.value = await loginApi(payload)
|
||||
// 로그인 — silent 옵션은 그대로 전달(에러 토스트 억제, 호출측 직접 처리)
|
||||
async function login(
|
||||
payload: LoginPayload,
|
||||
opts?: { silent?: boolean },
|
||||
): Promise<void> {
|
||||
user.value = await loginApi(payload, opts)
|
||||
}
|
||||
|
||||
// 회원가입 — 인증 메일 발송 후 대기. 자동 로그인하지 않으므로 user 는 갱신하지 않는다.
|
||||
|
||||
Reference in New Issue
Block a user