v0.1.45 - 로그아웃 API와 세션 상태 안내 추가
This commit is contained in:
52
src/App.vue
52
src/App.vue
@@ -14,6 +14,7 @@ import {
|
||||
confirmVerification,
|
||||
deleteAccount,
|
||||
fetchCurrentUser,
|
||||
logout as logoutRequest,
|
||||
confirmPasswordReset,
|
||||
login,
|
||||
persistAuthState,
|
||||
@@ -47,6 +48,7 @@ const authMode = ref('login')
|
||||
const authBusy = ref(false)
|
||||
const authMessage = ref('')
|
||||
const authToken = ref('')
|
||||
const authPersist = ref(false)
|
||||
const currentUser = ref(null)
|
||||
const goals = ref([])
|
||||
const goalQuery = ref('')
|
||||
@@ -622,6 +624,18 @@ const markedDateKeys = computed(() =>
|
||||
|
||||
const isAuthenticated = computed(() => Boolean(authToken.value && currentUser.value))
|
||||
const isAdmin = computed(() => currentUser.value?.role === 'admin')
|
||||
const authSessionInfo = computed(() => ({
|
||||
storageLabel: authPersist.value ? '이 기기에서 로그인 유지 중' : '브라우저를 닫으면 로그아웃',
|
||||
storageDescription: authPersist.value
|
||||
? '현재 기기에서는 localStorage에 로그인 상태를 저장합니다.'
|
||||
: '현재 기기에서는 sessionStorage에만 로그인 상태를 유지합니다.',
|
||||
lastLoginLabel: formatSessionDate(currentUser.value?.lastLoginAt),
|
||||
verificationLabel: currentUser.value?.role === 'admin'
|
||||
? '관리자 기본 계정'
|
||||
: currentUser.value?.emailVerifiedAt
|
||||
? '이메일 인증 완료'
|
||||
: '이메일 인증 필요',
|
||||
}))
|
||||
const filteredGoals = computed(() => {
|
||||
const query = goalQuery.value.trim().toLowerCase()
|
||||
return goals.value.filter((goal) => {
|
||||
@@ -1443,6 +1457,20 @@ function syncProfileForm() {
|
||||
profileForm.email = currentUser.value?.email ?? ''
|
||||
}
|
||||
|
||||
function formatSessionDate(value) {
|
||||
if (!value) {
|
||||
return '기록이 없습니다.'
|
||||
}
|
||||
|
||||
const date = new Date(value)
|
||||
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return '기록이 없습니다.'
|
||||
}
|
||||
|
||||
return `${date.getFullYear()}. ${`${date.getMonth() + 1}`.padStart(2, '0')}. ${`${date.getDate()}`.padStart(2, '0')} ${`${date.getHours()}`.padStart(2, '0')}:${`${date.getMinutes()}`.padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function resetPasswordForm() {
|
||||
passwordForm.currentPassword = ''
|
||||
passwordForm.newPassword = ''
|
||||
@@ -1531,6 +1559,7 @@ function updateAuthField({ field, value }) {
|
||||
|
||||
async function applyAuthSuccess(data, persist = false) {
|
||||
authToken.value = data.token
|
||||
authPersist.value = persist
|
||||
currentUser.value = data.user
|
||||
setSyncFeedback('cloud', '클라우드 동기화 연결됨')
|
||||
persistAuthState({
|
||||
@@ -1607,6 +1636,7 @@ async function restoreAuthSession() {
|
||||
}
|
||||
|
||||
authToken.value = savedAuth.token
|
||||
authPersist.value = savedAuth.persist
|
||||
currentUser.value = savedAuth.user ?? null
|
||||
|
||||
try {
|
||||
@@ -1624,6 +1654,7 @@ async function restoreAuthSession() {
|
||||
syncProfileForm()
|
||||
} catch (error) {
|
||||
authToken.value = ''
|
||||
authPersist.value = false
|
||||
currentUser.value = null
|
||||
setSyncFeedback('local', '로그인 후 클라우드 저장을 사용할 수 있습니다.', {
|
||||
visible: false,
|
||||
@@ -1632,9 +1663,10 @@ async function restoreAuthSession() {
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
function clearAuthenticatedState() {
|
||||
clearPendingSyncTimers()
|
||||
authToken.value = ''
|
||||
authPersist.value = false
|
||||
currentUser.value = null
|
||||
goals.value = []
|
||||
goalQuery.value = ''
|
||||
@@ -1665,6 +1697,20 @@ function logout() {
|
||||
resetAccountDeleteForm()
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
const token = authToken.value
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
await logoutRequest(token)
|
||||
} catch (error) {
|
||||
console.warn('서버 로그아웃 처리에 실패했습니다.', error)
|
||||
}
|
||||
}
|
||||
|
||||
clearAuthenticatedState()
|
||||
}
|
||||
|
||||
async function loadAdminDashboard() {
|
||||
if (!authToken.value || !isAdmin.value) {
|
||||
adminUsers.value = []
|
||||
@@ -1880,6 +1926,7 @@ async function submitProfileForm() {
|
||||
persistAuthState({
|
||||
token: authToken.value,
|
||||
user: result.user,
|
||||
persist: authPersist.value,
|
||||
})
|
||||
syncProfileForm()
|
||||
await loadAdminDashboard()
|
||||
@@ -1942,7 +1989,7 @@ async function submitDeleteAccount() {
|
||||
currentPassword: accountDeleteForm.currentPassword,
|
||||
})
|
||||
resetAccountDeleteForm()
|
||||
logout()
|
||||
clearAuthenticatedState()
|
||||
authMessage.value = ''
|
||||
window.alert(result.message || '회원 탈퇴가 완료되었습니다.')
|
||||
} catch (error) {
|
||||
@@ -3051,6 +3098,7 @@ onBeforeUnmount(() => {
|
||||
:account-delete-form="accountDeleteForm"
|
||||
:account-delete-busy="accountDeleteBusy"
|
||||
:account-delete-message="accountDeleteMessage"
|
||||
:auth-session-info="authSessionInfo"
|
||||
:guide-tooltip-reset-message="guideTooltipResetMessage"
|
||||
:carryover-check-policy="carryoverCheckPolicy"
|
||||
@update:profile-field="updateProfileField"
|
||||
|
||||
@@ -18,6 +18,10 @@ const props = defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
authSessionInfo: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
profileBusy: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -151,6 +155,28 @@ function updateAccountDeleteField(field, event) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 rounded-[24px] border border-stone-200 bg-white/80 p-4">
|
||||
<p class="text-[10px] font-bold tracking-[0.18em] text-stone-500">LOGIN STATUS</p>
|
||||
<div class="mt-3 space-y-3">
|
||||
<div class="rounded-2xl border border-stone-200 bg-[#fbf7f0] px-4 py-3">
|
||||
<p class="text-[10px] font-bold tracking-[0.16em] text-stone-500">현재 기기 저장 방식</p>
|
||||
<p class="mt-2 text-sm font-semibold leading-6 text-stone-800">{{ authSessionInfo.storageLabel }}</p>
|
||||
<p class="mt-1 text-xs font-semibold leading-5 text-stone-500">{{ authSessionInfo.storageDescription }}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-stone-200 bg-[#fbf7f0] px-4 py-3">
|
||||
<p class="text-[10px] font-bold tracking-[0.16em] text-stone-500">최근 로그인</p>
|
||||
<p class="mt-2 text-sm font-semibold leading-6 text-stone-800">{{ authSessionInfo.lastLoginLabel }}</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="authSessionInfo.verificationLabel"
|
||||
class="rounded-2xl border border-stone-200 bg-[#fbf7f0] px-4 py-3"
|
||||
>
|
||||
<p class="text-[10px] font-bold tracking-[0.16em] text-stone-500">이메일 인증</p>
|
||||
<p class="mt-2 text-sm font-semibold leading-6 text-stone-800">{{ authSessionInfo.verificationLabel }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="grid gap-6">
|
||||
|
||||
@@ -92,6 +92,13 @@ export async function fetchCurrentUser(token) {
|
||||
})
|
||||
}
|
||||
|
||||
export async function logout(token) {
|
||||
return request('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
token,
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateProfile(token, { email, nickname }) {
|
||||
return request('/api/auth/profile', {
|
||||
method: 'PUT',
|
||||
|
||||
Reference in New Issue
Block a user