본문 바로가기
카테고리 없음

로그인이 안되어 있을경우 로그인 화면이 보여지도록 vue3 코드를 만들어줘

by 두잇건강 2025. 8. 20.
반응형

Vue 3에서 로그인 상태에 따라 로그인 화면과 메인 화면을 조건부로 보여주는 코드를 만들어드리겠습니다.

방법 1: App.vue에서 전역 조건부 렌더링

App.vue (전역 로그인 체크)






      로딩 중...














import { onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
import LoginView from '@/views/LoginView.vue'
import NavBar from '@/components/NavBar.vue'

const authStore = useAuthStore()

onMounted(async () => {
  // 앱 시작 시 인증 상태 확인
  await authStore.initializeAuth()
})



#app {
  min-height: 100vh;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

.loading-container {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  background-color: #f8f9fa;
}

.loading-spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #007bff;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin-bottom: 1rem;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.authenticated-app {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

LoginView.vue (전용 로그인 페이지)






          로그인
          계정에 로그인하세요




            이메일




            비밀번호




            {{ authStore.loading ? '로그인 중...' : '로그인' }}





          {{ authStore.error }}




          비밀번호를 잊으셨나요?

            계정이 없으신가요? 회원가입








import { reactive } from 'vue'
import { useAuthStore } from '@/stores/auth'

const authStore = useAuthStore()

const loginForm = reactive({
  email: '',
  password: ''
})

const handleLogin = async () => {
  const result = await authStore.login({
    email: loginForm.email,
    password: loginForm.password
  })

  if (result.success) {
    // 로그인 성공 시 자동으로 메인 화면으로 전환됨 (App.vue의 조건부 렌더링)
    console.log('로그인 성공!')
  }
}

const showSignup = () => {
  // 회원가입 모달이나 페이지 표시 로직
  alert('회원가입 기능을 구현하세요')
}



.login-page {
  min-height: 100vh;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
}

.login-container {
  width: 100%;
  max-width: 400px;
}

.login-card {
  background: white;
  border-radius: 12px;
  padding: 2rem;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}

.login-header {
  text-align: center;
  margin-bottom: 2rem;
}

.login-header h1 {
  color: #333;
  margin-bottom: 0.5rem;
  font-size: 2rem;
}

.login-header p {
  color: #666;
  margin: 0;
}

.login-form {
  margin-bottom: 1.5rem;
}

.form-group {
  margin-bottom: 1.5rem;
}

.form-group label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 500;
  color: #333;
}

.form-group input {
  width: 100%;
  padding: 0.75rem;
  border: 2px solid #e1e5e9;
  border-radius: 8px;
  font-size: 1rem;
  transition: border-color 0.3s ease;
  box-sizing: border-box;
}

.form-group input:focus {
  outline: none;
  border-color: #667eea;
}

.form-group input:disabled {
  background-color: #f8f9fa;
  cursor: not-allowed;
}

.login-button {
  width: 100%;
  padding: 0.875rem;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: opacity 0.3s ease;
}

.login-button:hover:not(:disabled) {
  opacity: 0.9;
}

.login-button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.error-message {
  background-color: #fee;
  color: #c53030;
  padding: 0.75rem;
  border-radius: 8px;
  margin-bottom: 1rem;
  border: 1px solid #feb2b2;
  text-align: center;
  font-size: 0.9rem;
}

.login-footer {
  text-align: center;
  color: #666;
}

.forgot-password {
  display: block;
  margin-bottom: 1rem;
  color: #667eea;
  text-decoration: none;
  font-size: 0.9rem;
}

.forgot-password:hover {
  text-decoration: underline;
}

.signup-link {
  font-size: 0.9rem;
}

.signup-link a {
  color: #667eea;
  text-decoration: none;
  font-weight: 600;
}

.signup-link a:hover {
  text-decoration: underline;
}

NavBar.vue (로그인 후 네비게이션)





        My App



        대시보드
        프로필




          {{ authStore.userName || authStore.userEmail }}


              {{ role.replace('ROLE_', '') }}




          {{ authStore.loading ? '로그아웃 중...' : '로그아웃' }}







import { useAuthStore } from '@/stores/auth'

const authStore = useAuthStore()

const handleLogout = async () => {
  await authStore.logout()
}



.navbar {
  background: white;
  border-bottom: 1px solid #e1e5e9;
  padding: 0 1rem;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.navbar-container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  max-width: 1200px;
  margin: 0 auto;
  padding: 1rem 0;
}

.navbar-brand h2 {
  margin: 0;
  color: #333;
}

.navbar-menu {
  display: flex;
  gap: 2rem;
}

.nav-link {
  text-decoration: none;
  color: #666;
  font-weight: 500;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  transition: all 0.3s ease;
}

.nav-link:hover,
.nav-link.router-link-active {
  color: #667eea;
  background-color: #f8f9ff;
}

.navbar-user {
  display: flex;
  align-items: center;
  gap: 1rem;
}

.user-info {
  text-align: right;
}

.user-roles {
  display: flex;
  gap: 0.25rem;
  margin-top: 0.25rem;
  justify-content: flex-end;
}

.role-badge {
  background-color: #e3f2fd;
  color: #1976d2;
  padding: 0.125rem 0.375rem;
  border-radius: 12px;
  font-size: 0.75rem;
  font-weight: 500;
}

.logout-btn {
  padding: 0.5rem 1rem;
  background-color: #dc3545;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.9rem;
  transition: background-color 0.3s ease;
}

.logout-btn:hover:not(:disabled) {
  background-color: #c82333;
}

.logout-btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

방법 2: 라우터 기반 접근 방식

router/index.js (라우터 가드 강화)

import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import LoginView from '@/views/LoginView.vue'
import DashboardView from '@/views/DashboardView.vue'
import ProfileView from '@/views/ProfileView.vue'

const routes = [
  {
    path: '/login',
    name: 'Login',
    component: LoginView,
    meta: { 
      requiresAuth: false,
      hideNavbar: true 
    }
  },
  {
    path: '/',
    redirect: '/dashboard'
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: DashboardView,
    meta: { requiresAuth: true }
  },
  {
    path: '/profile',
    name: 'Profile',
    component: ProfileView,
    meta: { requiresAuth: true }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

router.beforeEach(async (to, from, next) => {
  const authStore = useAuthStore()

  // 인증 상태가 아직 확인되지 않은 경우 초기화
  if (!authStore.initialized) {
    await authStore.initializeAuth()
  }

  const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
  const isAuthenticated = authStore.isAuthenticated

  if (requiresAuth && !isAuthenticated) {
    // 인증이 필요한데 로그인하지 않은 경우
    next('/login')
  } else if (to.path === '/login' && isAuthenticated) {
    // 이미 로그인한 상태에서 로그인 페이지 접근
    next('/dashboard')
  } else {
    next()
  }
})

export default router

stores/auth.js (초기화 상태 추가)

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import axios from 'axios'

export const useAuthStore = defineStore('auth', () => {
  // 상태
  const user = ref(null)
  const token = ref(localStorage.getItem('jwt-token'))
  const loading = ref(false)
  const error = ref('')
  const initialized = ref(false) // 초기화 상태 추가

  // 계산된 속성
  const isAuthenticated = computed(() => !!token.value && !!user.value)
  const userEmail = computed(() => user.value?.email || '')
  const userName = computed(() => user.value?.name || '')
  const userRoles = computed(() => user.value?.roles || [])
  const isAdmin = computed(() => userRoles.value.includes('ROLE_ADMIN'))

  // 인증 초기화
  const initializeAuth = async () => {
    if (initialized.value) return

    loading.value = true

    try {
      const savedToken = localStorage.getItem('jwt-token')
      const savedUser = localStorage.getItem('user-info')

      if (savedToken && savedUser) {
        token.value = savedToken
        user.value = JSON.parse(savedUser)
        setAuthToken(savedToken)

        // 토큰 유효성 검사
        try {
          await axios.get('/api/user/profile')
          // 토큰이 유효하면 사용자 정보 최신화
        } catch (error) {
          if (error.response?.status === 401) {
            // 토큰이 만료되었거나 유효하지 않음
            clearAuthData()
          }
        }
      }
    } catch (error) {
      console.error('인증 초기화 실패:', error)
      clearAuthData()
    } finally {
      loading.value = false
      initialized.value = true
    }
  }

  // 로그인
  const login = async (credentials) => {
    loading.value = true
    error.value = ''

    try {
      const response = await axios.post('/api/authenticate', credentials)

      if (response.data.jwt) {
        token.value = response.data.jwt
        user.value = {
          email: response.data.email || credentials.email,
          name: response.data.name || '',
          roles: response.data.roles || ['ROLE_USER'],
          id: response.data.userId || null
        }

        localStorage.setItem('jwt-token', response.data.jwt)
        localStorage.setItem('user-info', JSON.stringify(user.value))
        setAuthToken(response.data.jwt)

        return { success: true }
      }
    } catch (err) {
      error.value = err.response?.status === 401 
        ? '이메일 또는 비밀번호가 올바르지 않습니다.' 
        : '로그인 중 오류가 발생했습니다.'
      return { success: false, error: error.value }
    } finally {
      loading.value = false
    }
  }

  // 로그아웃
  const logout = async () => {
    loading.value = true

    try {
      await axios.post('/api/logout')
    } catch (err) {
      console.error('로그아웃 API 호출 실패:', err)
    } finally {
      clearAuthData()
      loading.value = false
    }
  }

  // 인증 데이터 정리
  const clearAuthData = () => {
    user.value = null
    token.value = null
    error.value = ''
    localStorage.removeItem('jwt-token')
    localStorage.removeItem('user-info')
    delete axios.defaults.headers.common['Authorization']
  }

  const setAuthToken = (authToken) => {
    if (authToken) {
      axios.defaults.headers.common['Authorization'] = `Bearer ${authToken}`
    } else {
      delete axios.defaults.headers.common['Authorization']
    }
  }

  return {
    // 상태
    user,
    token,
    loading,
    error,
    initialized,

    // 계산된 속성
    isAuthenticated,
    userEmail,
    userName,
    userRoles,
    isAdmin,

    // 액션
    login,
    logout,
    initializeAuth,
    clearAuthData
  }
})

방법 3: 컴포넌트 레벨 조건부 렌더링

AuthGuard.vue (인증 가드 컴포넌트)





    인증 확인 중...














import { onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
import LoginView from '@/views/LoginView.vue'

const authStore = useAuthStore()

onMounted(async () => {
  if (!authStore.initialized) {
    await authStore.initializeAuth()
  }
})



.auth-loading {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  background-color: #f8f9fa;
}

.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #007bff;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin-bottom: 1rem;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

사용 예시











import AuthGuard from '@/components/AuthGuard.vue'
import NavBar from '@/components/NavBar.vue'

핵심 특징

전역 인증 체크:

  • 앱 시작 시 자동으로 인증 상태 확인
  • localStorage의 토큰 유효성 검사
  • 조건부 렌더링으로 적절한 화면 표시

사용자 경험 개선:

  • 로딩 상태 표시
  • 자동 리디렉션
  • 에러 메시지 처리

보안 강화:

  • 토큰 유효성 실시간 검증
  • 만료된 토큰 자동 처리
  • 라우터 가드로 추가 보호

이 코드를 사용하면 로그인 상태에 따라 자동으로 적절한 화면이 표시됩니다!

반응형