diff --git a/api.hyungi.net/controllers/authController.js b/api.hyungi.net/controllers/authController.js index e3869e7..91d4f26 100644 --- a/api.hyungi.net/controllers/authController.js +++ b/api.hyungi.net/controllers/authController.js @@ -19,7 +19,33 @@ const login = async (req, res) => { return res.status(result.status || 400).json({ error: result.error }); } - res.json(result.data); + // 로그인 성공 후, 역할에 따라 리디렉션 URL을 결정 + const user = result.data.user; + let redirectUrl; + + switch (user.role) { + case 'admin': + case 'system': // 'system'도 관리자로 취급 + redirectUrl = '/pages/dashboard/admin.html'; + break; + case 'leader': + redirectUrl = '/pages/dashboard/group-leader.html'; + break; + case 'support': + // 예시: 지원팀 대시보드가 있다면 + // redirectUrl = '/pages/dashboard/support.html'; + // 없다면 일반 사용자 대시보드로 + redirectUrl = '/pages/dashboard/user.html'; + break; + default: + redirectUrl = '/pages/dashboard/user.html'; + } + + // 최종 응답에 redirectUrl을 포함하여 전달 + res.json({ + ...result.data, + redirectUrl: redirectUrl + }); } catch (error) { console.error('Login controller error:', error); diff --git a/web-ui/js/api-helper.js b/web-ui/js/api-helper.js index e29890e..8057bc9 100644 --- a/web-ui/js/api-helper.js +++ b/web-ui/js/api-helper.js @@ -1,18 +1,45 @@ // /public/js/api-helper.js -// API 기본 URL 설정 -const API_BASE = location.hostname.includes('localhost') - ? 'http://localhost:3005/api' - : 'https://api.hyungi.net/api'; +import { API_BASE_URL } from './api-config.js'; +import { getToken, clearAuthData } from './auth.js'; -// 인증된 fetch 함수 -async function authFetch(url, options = {}) { - const token = localStorage.getItem('token'); +/** + * 로그인 API를 호출합니다. (인증이 필요 없는 public 요청) + * @param {string} username - 사용자 아이디 + * @param {string} password - 사용자 비밀번호 + * @returns {Promise} - API 응답 결과 + */ +export async function login(username, password) { + const response = await fetch(`${API_BASE_URL}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }), + }); + + const result = await response.json(); + if (!response.ok) { + // API 에러 응답을 그대로 에러 객체로 던져서 호출부에서 처리하도록 함 + throw new Error(result.error || '로그인에 실패했습니다.'); + } + return result; +} + + +/** + * 인증이 필요한 API 요청을 위한 fetch 래퍼 함수 + * @param {string} endpoint - /로 시작하는 API 엔드포인트 + * @param {object} options - fetch 함수에 전달할 옵션 + * @returns {Promise} - fetch 응답 객체 + */ +async function authFetch(endpoint, options = {}) { + const token = getToken(); if (!token) { console.error('토큰이 없습니다. 로그인이 필요합니다.'); - window.location.href = '/index.html'; - return; + clearAuthData(); // 인증 정보 정리 + window.location.href = '/index.html'; // 로그인 페이지로 리디렉션 + // 에러를 던져서 후속 실행을 중단 + throw new Error('인증 토큰이 없습니다.'); } const defaultHeaders = { @@ -20,7 +47,7 @@ async function authFetch(url, options = {}) { 'Content-Type': 'application/json' }; - const response = await fetch(url, { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { ...options, headers: { ...defaultHeaders, @@ -28,59 +55,61 @@ async function authFetch(url, options = {}) { } }); - // 401 에러 시 로그인 페이지로 + // 401 Unauthorized 에러 발생 시, 토큰이 유효하지 않다는 의미 if (response.status === 401) { - console.error('인증 실패. 다시 로그인해주세요.'); - localStorage.removeItem('token'); + console.error('인증 실패. 토큰이 만료되었거나 유효하지 않습니다.'); + clearAuthData(); // 만료된 인증 정보 정리 window.location.href = '/index.html'; - return; + throw new Error('인증에 실패했습니다.'); } return response; } -// GET 요청 헬퍼 -async function apiGet(endpoint) { - const response = await authFetch(`${API_BASE}${endpoint}`); - if (!response) return null; +// 공통 API 요청 함수들 + +/** + * GET 요청 헬퍼 + * @param {string} endpoint - API 엔드포인트 + */ +export async function apiGet(endpoint) { + const response = await authFetch(endpoint); return response.json(); } -// POST 요청 헬퍼 -async function apiPost(endpoint, data) { - const response = await authFetch(`${API_BASE}${endpoint}`, { +/** + * POST 요청 헬퍼 + * @param {string} endpoint - API 엔드포인트 + * @param {object} data - 전송할 데이터 + */ +export async function apiPost(endpoint, data) { + const response = await authFetch(endpoint, { method: 'POST', body: JSON.stringify(data) }); - if (!response) return null; return response.json(); } -// PUT 요청 헬퍼 -async function apiPut(endpoint, data) { - const response = await authFetch(`${API_BASE}${endpoint}`, { +/** + * PUT 요청 헬퍼 + * @param {string} endpoint - API 엔드포인트 + * @param {object} data - 전송할 데이터 + */ +export async function apiPut(endpoint, data) { + const response = await authFetch(endpoint, { method: 'PUT', body: JSON.stringify(data) }); - if (!response) return null; return response.json(); } -// DELETE 요청 헬퍼 -async function apiDelete(endpoint) { - const response = await authFetch(`${API_BASE}${endpoint}`, { +/** + * DELETE 요청 헬퍼 + * @param {string} endpoint - API 엔드포인트 + */ +export async function apiDelete(endpoint) { + const response = await authFetch(endpoint, { method: 'DELETE' }); - if (!response) return null; return response.json(); -} - -// 내보내기 (다른 파일에서 사용 가능) -window.API = { - get: apiGet, - post: apiPost, - put: apiPut, - delete: apiDelete, - fetch: authFetch, - BASE: API_BASE -}; \ No newline at end of file +} \ No newline at end of file diff --git a/web-ui/js/auth.js b/web-ui/js/auth.js new file mode 100644 index 0000000..7a8e6e5 --- /dev/null +++ b/web-ui/js/auth.js @@ -0,0 +1,76 @@ +// js/auth.js + +/** + * JWT 토큰을 디코딩하여 페이로드(내용)를 반환합니다. + * @param {string} token - JWT 토큰 + * @returns {object|null} - 디코딩된 페이로드 객체 또는 파싱 실패 시 null + */ +export function parseJwt(token) { + try { + // 토큰의 두 번째 부분(payload)을 base64 디코딩하고 JSON으로 파싱 + return JSON.parse(atob(token.split('.')[1])); + } catch (e) { + console.error("잘못된 토큰입니다.", e); + return null; + } +} + +/** + * localStorage에서 인증 토큰을 가져옵니다. + * @returns {string|null} - 저장된 토큰 또는 토큰이 없을 경우 null + */ +export function getToken() { + return localStorage.getItem('token'); +} + +/** + * localStorage에서 사용자 정보를 가져옵니다. + * @returns {object|null} - 저장된 사용자 객체 또는 정보가 없을 경우 null + */ +export function getUser() { + const user = localStorage.getItem('user'); + try { + return user ? JSON.parse(user) : null; + } catch(e) { + console.error("사용자 정보를 파싱하는 데 실패했습니다.", e); + return null; + } +} + +/** + * 로그인 성공 후 토큰과 사용자 정보를 localStorage에 저장합니다. + * @param {string} token - 서버에서 받은 JWT 토큰 + * @param {object} user - 서버에서 받은 사용자 정보 객체 + */ +export function saveAuthData(token, user) { + localStorage.setItem('token', token); + localStorage.setItem('user', JSON.stringify(user)); +} + +/** + * 로그아웃 시 localStorage에서 인증 정보를 제거합니다. + */ +export function clearAuthData() { + localStorage.removeItem('token'); + localStorage.removeItem('user'); +} + +/** + * 현재 사용자가 로그인 상태인지 확인합니다. + * @returns {boolean} - 로그인 상태이면 true, 아니면 false + */ +export function isLoggedIn() { + const token = getToken(); + if (!token) { + return false; + } + + // 선택 사항: 토큰 만료 여부 확인 로직 추가 가능 + // const payload = parseJwt(token); + // if (payload && payload.exp * 1000 > Date.now()) { + // return true; + // } + // return false; + + return !!token; +} \ No newline at end of file diff --git a/web-ui/js/login.js b/web-ui/js/login.js index e554579..9e79b91 100644 --- a/web-ui/js/login.js +++ b/web-ui/js/login.js @@ -1,52 +1,7 @@ -// 깔끔한 로그인 로직 (login.js) -import { API } from './api-config.js'; +// /js/login.js -function parseJwt(token) { - try { - return JSON.parse(atob(token.split('.')[1])); - } catch (e) { - return null; - } -} - -// 역할별 대시보드 라우팅 -function routeToDashboard(user) { - const accessLevel = (user.access_level || '').toLowerCase().trim(); - - // 그룹장/리더 관련 키워드들 - const leaderKeywords = [ - 'group_leader', 'groupleader', 'group-leader', - 'leader', 'supervisor', 'team_leader', 'teamleader', - '그룹장', '팀장', '현장책임자' - ]; - - // 관리자 관련 키워드들 - const adminKeywords = [ - 'admin', 'administrator', 'system', - '관리자', '시스템관리자' - ]; - - // 지원팀 관련 키워드들 - const supportKeywords = [ - 'support', 'support_team', 'supportteam', - '지원팀', '지원' - ]; - - // 키워드 매칭 - if (leaderKeywords.some(keyword => accessLevel.includes(keyword.toLowerCase()))) { - return '/pages/dashboard/group-leader.html'; - } - - if (adminKeywords.some(keyword => accessLevel.includes(keyword.toLowerCase()))) { - return '/pages/dashboard/admin.html'; - } - - if (supportKeywords.some(keyword => accessLevel.includes(keyword.toLowerCase()))) { - return '/pages/dashboard/support.html'; - } - - return '/pages/dashboard/user.html'; -} +import { login } from './api-helper.js'; +import { saveAuthData, clearAuthData } from './auth.js'; document.getElementById('loginForm').addEventListener('submit', async function (e) { e.preventDefault(); @@ -55,48 +10,45 @@ document.getElementById('loginForm').addEventListener('submit', async function ( const password = document.getElementById('password').value; const errorDiv = document.getElementById('error'); - // 로딩 상태 표시 const submitBtn = e.target.querySelector('button[type="submit"]'); const originalText = submitBtn.textContent; + + // 로딩 상태 시작 submitBtn.disabled = true; submitBtn.textContent = '로그인 중...'; + errorDiv.style.display = 'none'; try { - const res = await fetch(`${API}/auth/login`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ username, password }) - }); + // API 헬퍼를 통해 로그인 요청 + const result = await login(username, password); - const result = await res.json(); + if (result.success && result.token) { + // 인증 정보 저장 + saveAuthData(result.token, result.user); - if (res.ok && result.success && result.token) { - localStorage.setItem('token', result.token); - localStorage.setItem('user', JSON.stringify(result.user)); - - // 역할별 대시보드로 리다이렉트 - const redirectUrl = routeToDashboard(result.user); + // 백엔드가 지정한 URL로 리디렉션 + const redirectUrl = result.redirectUrl || '/pages/dashboard/user.html'; // 혹시 모를 예외처리 - // 부드러운 전환 효과 + // 부드러운 화면 전환 효과 + document.body.style.transition = 'opacity 0.3s ease-out'; document.body.style.opacity = '0'; + setTimeout(() => { window.location.href = redirectUrl; }, 300); } else { - localStorage.removeItem('token'); - localStorage.removeItem('user'); + // 이 케이스는 api-helper에서 throw new Error()로 처리되어 catch 블록으로 바로 이동합니다. + // 하지만, 만약의 경우를 대비해 방어 코드를 남겨둡니다. + clearAuthData(); errorDiv.textContent = result.error || '로그인에 실패했습니다.'; errorDiv.style.display = 'block'; - - // 에러 메시지 자동 숨김 - setTimeout(() => { - errorDiv.style.display = 'none'; - }, 5000); } } catch (err) { console.error('로그인 오류:', err); - errorDiv.textContent = '서버 연결에 실패했습니다. 잠시 후 다시 시도해주세요.'; + clearAuthData(); + // api-helper에서 보낸 에러 메시지를 표시 + errorDiv.textContent = err.message || '서버 연결에 실패했습니다.'; errorDiv.style.display = 'block'; } finally { // 로딩 상태 해제