feat: 토큰 만료 시 자동 로그아웃 기능 추가 및 테이블명 수정
🔐 토큰 만료 시 자동 로그아웃 기능: 1. JWT 토큰 만료 시간 대폭 연장: - 액세스 토큰: 24시간 → 7일 - 리프레시 토큰: 7일 → 30일 - 사용자 편의성 크게 향상 2. 토큰 만료 감지 및 처리: - isTokenExpired() 함수 추가 - JWT 페이로드 파싱하여 exp 확인 - 현재 시간과 비교하여 만료 여부 판단 3. 자동 로그아웃 처리: - API 호출 시 401 오류 감지 - 주기적 토큰 만료 확인 (5분마다) - 만료 시 자동 인증 데이터 정리 - 사용자 알림 후 로그인 페이지 리다이렉트 4. 개선된 인증 데이터 관리: - clearAuthData() 함수로 통합 관리 - token, user, userInfo, currentUser 모두 정리 - 메모리 누수 방지 🐛 데이터베이스 테이블명 수정: 1. projectModel.js: - Projects → projects (대문자 → 소문자) - 실제 DB 테이블명과 일치 2. taskModel.js: - Tasks → tasks (대문자 → 소문자) - 실제 DB 테이블명과 일치 3. API 오류 해결: - '테이블이 존재하지 않습니다' 오류 수정 - projects, tasks API 정상 작동 ✅ 사용자 경험 개선: - 토큰 만료로 인한 예상치 못한 오류 방지 - 명확한 만료 알림 메시지 - 자동 로그아웃으로 보안 강화 - 더 긴 세션 유지로 편의성 향상 🔧 기술적 개선: - JWT 페이로드 안전한 파싱 - 에러 핸들링 강화 - 주기적 백그라운드 확인 - 전역 함수로 재사용성 향상 🎯 결과: - 안정적인 인증 시스템 - 사용자 친화적인 세션 관리 - 보안성과 편의성의 균형 - API 호출 오류 해결 테스트: - 토큰 만료 후 자동 로그아웃 확인 - projects, tasks API 정상 작동 확인
This commit is contained in:
@@ -10,7 +10,7 @@ const create = async (project, callback) => {
|
|||||||
} = project;
|
} = project;
|
||||||
|
|
||||||
const [result] = await db.query(
|
const [result] = await db.query(
|
||||||
`INSERT INTO Projects
|
`INSERT INTO projects
|
||||||
(job_no, project_name, contract_date, due_date, delivery_method, site, pm)
|
(job_no, project_name, contract_date, due_date, delivery_method, site, pm)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[job_no, project_name, contract_date, due_date, delivery_method, site, pm]
|
[job_no, project_name, contract_date, due_date, delivery_method, site, pm]
|
||||||
@@ -26,7 +26,7 @@ const getAll = async (callback) => {
|
|||||||
try {
|
try {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const [rows] = await db.query(
|
const [rows] = await db.query(
|
||||||
`SELECT * FROM Projects ORDER BY project_id DESC`
|
`SELECT * FROM projects ORDER BY project_id DESC`
|
||||||
);
|
);
|
||||||
callback(null, rows);
|
callback(null, rows);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -38,7 +38,7 @@ const getById = async (project_id, callback) => {
|
|||||||
try {
|
try {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const [rows] = await db.query(
|
const [rows] = await db.query(
|
||||||
`SELECT * FROM Projects WHERE project_id = ?`,
|
`SELECT * FROM projects WHERE project_id = ?`,
|
||||||
[project_id]
|
[project_id]
|
||||||
);
|
);
|
||||||
callback(null, rows[0]);
|
callback(null, rows[0]);
|
||||||
@@ -57,7 +57,7 @@ const update = async (project, callback) => {
|
|||||||
} = project;
|
} = project;
|
||||||
|
|
||||||
const [result] = await db.query(
|
const [result] = await db.query(
|
||||||
`UPDATE Projects
|
`UPDATE projects
|
||||||
SET job_no = ?,
|
SET job_no = ?,
|
||||||
project_name = ?,
|
project_name = ?,
|
||||||
contract_date = ?,
|
contract_date = ?,
|
||||||
@@ -79,7 +79,7 @@ const remove = async (project_id, callback) => {
|
|||||||
try {
|
try {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const [result] = await db.query(
|
const [result] = await db.query(
|
||||||
`DELETE FROM Projects WHERE project_id = ?`,
|
`DELETE FROM projects WHERE project_id = ?`,
|
||||||
[project_id]
|
[project_id]
|
||||||
);
|
);
|
||||||
callback(null, result.affectedRows);
|
callback(null, result.affectedRows);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const create = async (task, callback) => {
|
|||||||
const { category, subcategory, task_name, description } = task;
|
const { category, subcategory, task_name, description } = task;
|
||||||
|
|
||||||
const [result] = await db.query(
|
const [result] = await db.query(
|
||||||
`INSERT INTO Tasks (category, subcategory, task_name, description)
|
`INSERT INTO tasks (category, subcategory, task_name, description)
|
||||||
VALUES (?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?)`,
|
||||||
[category, subcategory, task_name, description]
|
[category, subcategory, task_name, description]
|
||||||
);
|
);
|
||||||
@@ -23,7 +23,7 @@ const getAll = async (callback) => {
|
|||||||
try {
|
try {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const [rows] = await db.query(
|
const [rows] = await db.query(
|
||||||
`SELECT * FROM Tasks ORDER BY task_id DESC`
|
`SELECT * FROM tasks ORDER BY task_id DESC`
|
||||||
);
|
);
|
||||||
callback(null, rows);
|
callback(null, rows);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -36,7 +36,7 @@ const getById = async (task_id, callback) => {
|
|||||||
try {
|
try {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const [rows] = await db.query(
|
const [rows] = await db.query(
|
||||||
`SELECT * FROM Tasks WHERE task_id = ?`,
|
`SELECT * FROM tasks WHERE task_id = ?`,
|
||||||
[task_id]
|
[task_id]
|
||||||
);
|
);
|
||||||
callback(null, rows[0]);
|
callback(null, rows[0]);
|
||||||
@@ -52,7 +52,7 @@ const update = async (task, callback) => {
|
|||||||
const { task_id, category, subcategory, task_name, description } = task;
|
const { task_id, category, subcategory, task_name, description } = task;
|
||||||
|
|
||||||
const [result] = await db.query(
|
const [result] = await db.query(
|
||||||
`UPDATE Tasks
|
`UPDATE tasks
|
||||||
SET category = ?,
|
SET category = ?,
|
||||||
subcategory = ?,
|
subcategory = ?,
|
||||||
task_name = ?,
|
task_name = ?,
|
||||||
@@ -72,7 +72,7 @@ const remove = async (task_id, callback) => {
|
|||||||
try {
|
try {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const [result] = await db.query(
|
const [result] = await db.query(
|
||||||
`DELETE FROM Tasks WHERE task_id = ?`,
|
`DELETE FROM tasks WHERE task_id = ?`,
|
||||||
[task_id]
|
[task_id]
|
||||||
);
|
);
|
||||||
callback(null, result.affectedRows);
|
callback(null, result.affectedRows);
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ services:
|
|||||||
- DB_NAME=hyungi
|
- DB_NAME=hyungi
|
||||||
- DB_ROOT_PASSWORD=tkfb2024!
|
- DB_ROOT_PASSWORD=tkfb2024!
|
||||||
- JWT_SECRET=tkfb_jwt_secret_2024_hyungi_secure_key
|
- JWT_SECRET=tkfb_jwt_secret_2024_hyungi_secure_key
|
||||||
|
- JWT_EXPIRES_IN=7d
|
||||||
|
- JWT_REFRESH_EXPIRES_IN=30d
|
||||||
volumes:
|
volumes:
|
||||||
- ./api.hyungi.net/public/img:/usr/src/app/public/img:ro
|
- ./api.hyungi.net/public/img:/usr/src/app/public/img:ro
|
||||||
- ./api.hyungi.net/uploads:/usr/src/app/uploads
|
- ./api.hyungi.net/uploads:/usr/src/app/uploads
|
||||||
|
|||||||
@@ -34,15 +34,45 @@ window.API_BASE_URL = API_URL;
|
|||||||
|
|
||||||
function ensureAuthenticated() {
|
function ensureAuthenticated() {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
if (!token || token === 'undefined') {
|
if (!token || token === 'undefined' || token === 'null') {
|
||||||
alert('로그인이 필요합니다');
|
console.log('🚨 인증되지 않은 사용자. 로그인 페이지로 이동합니다.');
|
||||||
localStorage.removeItem('token');
|
clearAuthData(); // 만약을 위해 한번 더 정리
|
||||||
window.location.href = '/';
|
window.location.href = '/index.html';
|
||||||
return null;
|
return false; // 이후 코드 실행 방지
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 토큰 만료 확인
|
||||||
|
if (isTokenExpired(token)) {
|
||||||
|
console.log('🚨 토큰이 만료되었습니다. 로그인 페이지로 이동합니다.');
|
||||||
|
clearAuthData();
|
||||||
|
alert('세션이 만료되었습니다. 다시 로그인해주세요.');
|
||||||
|
window.location.href = '/index.html';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 토큰 만료 확인 함수
|
||||||
|
function isTokenExpired(token) {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(atob(token.split('.')[1]));
|
||||||
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
return payload.exp < currentTime;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('토큰 파싱 오류:', error);
|
||||||
|
return true; // 파싱 실패 시 만료된 것으로 간주
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 인증 데이터 정리 함수
|
||||||
|
function clearAuthData() {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
localStorage.removeItem('userInfo');
|
||||||
|
localStorage.removeItem('currentUser');
|
||||||
|
}
|
||||||
|
|
||||||
function getAuthHeaders() {
|
function getAuthHeaders() {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
return {
|
return {
|
||||||
@@ -72,11 +102,11 @@ async function apiCall(url, options = {}) {
|
|||||||
|
|
||||||
// 인증 만료 처리
|
// 인증 만료 처리
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
console.error('❌ 인증 만료');
|
console.error('🚨 인증 실패: 토큰이 만료되었거나 유효하지 않습니다.');
|
||||||
localStorage.removeItem('token');
|
clearAuthData();
|
||||||
alert('인증이 만료되었습니다. 다시 로그인해주세요.');
|
alert('세션이 만료되었습니다. 다시 로그인해주세요.');
|
||||||
window.location.href = '/';
|
window.location.href = '/index.html';
|
||||||
return;
|
throw new Error('인증에 실패했습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 응답 실패 처리
|
// 응답 실패 처리
|
||||||
@@ -143,6 +173,8 @@ window.ensureAuthenticated = ensureAuthenticated;
|
|||||||
window.getAuthHeaders = getAuthHeaders;
|
window.getAuthHeaders = getAuthHeaders;
|
||||||
window.apiCall = apiCall;
|
window.apiCall = apiCall;
|
||||||
window.testApiConnection = testApiConnection;
|
window.testApiConnection = testApiConnection;
|
||||||
|
window.isTokenExpired = isTokenExpired;
|
||||||
|
window.clearAuthData = clearAuthData;
|
||||||
|
|
||||||
// 개발 모드에서 자동 테스트
|
// 개발 모드에서 자동 테스트
|
||||||
if (window.location.hostname === 'localhost' || window.location.hostname.startsWith('192.168.')) {
|
if (window.location.hostname === 'localhost' || window.location.hostname.startsWith('192.168.')) {
|
||||||
@@ -150,3 +182,14 @@ if (window.location.hostname === 'localhost' || window.location.hostname.startsW
|
|||||||
testApiConnection();
|
testApiConnection();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 주기적으로 토큰 만료 확인 (5분마다)
|
||||||
|
setInterval(() => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token && isTokenExpired(token)) {
|
||||||
|
console.log('🚨 주기적 확인: 토큰이 만료되었습니다.');
|
||||||
|
clearAuthData();
|
||||||
|
alert('세션이 만료되었습니다. 다시 로그인해주세요.');
|
||||||
|
window.location.href = '/index.html';
|
||||||
|
}
|
||||||
|
}, 5 * 60 * 1000); // 5분마다 확인
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
// daily-work-report.js - 통합 API 설정 적용 버전
|
// daily-work-report.js - 브라우저 호환 버전
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
// 🌐 통합 API 설정 import
|
// 🌐 API 설정 (window 객체에서 가져오기)
|
||||||
// =================================================================
|
// =================================================================
|
||||||
import { API, getAuthHeaders, apiCall } from '/js/api-config.js';
|
// API 설정은 api-config.js에서 window 객체에 설정됨
|
||||||
|
|
||||||
// 전역 변수
|
// 전역 변수
|
||||||
let workTypes = [];
|
let workTypes = [];
|
||||||
@@ -117,7 +117,7 @@ async function loadData() {
|
|||||||
async function loadWorkers() {
|
async function loadWorkers() {
|
||||||
try {
|
try {
|
||||||
console.log('Workers API 호출 중... (통합 API 사용)');
|
console.log('Workers API 호출 중... (통합 API 사용)');
|
||||||
const data = await apiCall(`${API}/workers`);
|
const data = await window.apiCall(`${window.API}/workers`);
|
||||||
workers = Array.isArray(data) ? data : (data.data || data.workers || []);
|
workers = Array.isArray(data) ? data : (data.data || data.workers || []);
|
||||||
console.log('✅ Workers 로드 성공:', workers.length);
|
console.log('✅ Workers 로드 성공:', workers.length);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -129,7 +129,7 @@ async function loadWorkers() {
|
|||||||
async function loadProjects() {
|
async function loadProjects() {
|
||||||
try {
|
try {
|
||||||
console.log('Projects API 호출 중... (통합 API 사용)');
|
console.log('Projects API 호출 중... (통합 API 사용)');
|
||||||
const data = await apiCall(`${API}/projects`);
|
const data = await window.apiCall(`${window.API}/projects`);
|
||||||
projects = Array.isArray(data) ? data : (data.data || data.projects || []);
|
projects = Array.isArray(data) ? data : (data.data || data.projects || []);
|
||||||
console.log('✅ Projects 로드 성공:', projects.length);
|
console.log('✅ Projects 로드 성공:', projects.length);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -140,7 +140,7 @@ async function loadProjects() {
|
|||||||
|
|
||||||
async function loadWorkTypes() {
|
async function loadWorkTypes() {
|
||||||
try {
|
try {
|
||||||
const data = await apiCall(`${API}/daily-work-reports/work-types`);
|
const data = await window.apiCall(`${window.API}/daily-work-reports/work-types`);
|
||||||
if (Array.isArray(data) && data.length > 0) {
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
workTypes = data;
|
workTypes = data;
|
||||||
console.log('✅ 작업 유형 API 사용 (통합 설정)');
|
console.log('✅ 작업 유형 API 사용 (통합 설정)');
|
||||||
@@ -159,7 +159,7 @@ async function loadWorkTypes() {
|
|||||||
|
|
||||||
async function loadWorkStatusTypes() {
|
async function loadWorkStatusTypes() {
|
||||||
try {
|
try {
|
||||||
const data = await apiCall(`${API}/daily-work-reports/work-status-types`);
|
const data = await window.apiCall(`${window.API}/daily-work-reports/work-status-types`);
|
||||||
if (Array.isArray(data) && data.length > 0) {
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
workStatusTypes = data;
|
workStatusTypes = data;
|
||||||
console.log('✅ 업무 상태 유형 API 사용 (통합 설정)');
|
console.log('✅ 업무 상태 유형 API 사용 (통합 설정)');
|
||||||
@@ -177,7 +177,7 @@ async function loadWorkStatusTypes() {
|
|||||||
|
|
||||||
async function loadErrorTypes() {
|
async function loadErrorTypes() {
|
||||||
try {
|
try {
|
||||||
const data = await apiCall(`${API}/daily-work-reports/error-types`);
|
const data = await window.apiCall(`${window.API}/daily-work-reports/error-types`);
|
||||||
if (Array.isArray(data) && data.length > 0) {
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
errorTypes = data;
|
errorTypes = data;
|
||||||
console.log('✅ 에러 유형 API 사용 (통합 설정)');
|
console.log('✅ 에러 유형 API 사용 (통합 설정)');
|
||||||
@@ -424,7 +424,7 @@ async function saveWorkReport() {
|
|||||||
console.log('전송 데이터 (통합 API 사용):', requestData);
|
console.log('전송 데이터 (통합 API 사용):', requestData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await apiCall(`${API}/daily-work-reports`, {
|
const result = await window.apiCall(`${window.API}/daily-work-reports`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(requestData)
|
body: JSON.stringify(requestData)
|
||||||
});
|
});
|
||||||
@@ -510,7 +510,7 @@ async function loadTodayWorkers() {
|
|||||||
|
|
||||||
console.log(`🔒 본인 입력분만 조회 (통합 API): ${API}/daily-work-reports?${queryParams}`);
|
console.log(`🔒 본인 입력분만 조회 (통합 API): ${API}/daily-work-reports?${queryParams}`);
|
||||||
|
|
||||||
const rawData = await apiCall(`${API}/daily-work-reports?${queryParams}`);
|
const rawData = await window.apiCall(`${window.API}/daily-work-reports?${queryParams}`);
|
||||||
console.log('📊 당일 작업 데이터 (통합 API):', rawData);
|
console.log('📊 당일 작업 데이터 (통합 API):', rawData);
|
||||||
|
|
||||||
let data = [];
|
let data = [];
|
||||||
@@ -645,7 +645,7 @@ async function editWorkItem(workId) {
|
|||||||
// 1. 기존 데이터 조회 (통합 API 사용)
|
// 1. 기존 데이터 조회 (통합 API 사용)
|
||||||
showMessage('작업 정보를 불러오는 중... (통합 API)', 'loading');
|
showMessage('작업 정보를 불러오는 중... (통합 API)', 'loading');
|
||||||
|
|
||||||
const workData = await apiCall(`${API}/daily-work-reports/${workId}`);
|
const workData = await window.apiCall(`${window.API}/daily-work-reports/${workId}`);
|
||||||
console.log('수정할 작업 데이터 (통합 API):', workData);
|
console.log('수정할 작업 데이터 (통합 API):', workData);
|
||||||
|
|
||||||
// 2. 수정 모달 표시
|
// 2. 수정 모달 표시
|
||||||
@@ -784,7 +784,7 @@ async function saveEditedWork() {
|
|||||||
|
|
||||||
showMessage('작업을 수정하는 중... (통합 API)', 'loading');
|
showMessage('작업을 수정하는 중... (통합 API)', 'loading');
|
||||||
|
|
||||||
const result = await apiCall(`${API}/daily-work-reports/${editingWorkId}`, {
|
const result = await window.apiCall(`${window.API}/daily-work-reports/${editingWorkId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify(updateData)
|
body: JSON.stringify(updateData)
|
||||||
});
|
});
|
||||||
@@ -813,7 +813,7 @@ async function deleteWorkItem(workId) {
|
|||||||
showMessage('작업을 삭제하는 중... (통합 API)', 'loading');
|
showMessage('작업을 삭제하는 중... (통합 API)', 'loading');
|
||||||
|
|
||||||
// 개별 항목 삭제 API 호출 (본인 작성분만 삭제 가능) - 통합 API 사용
|
// 개별 항목 삭제 API 호출 (본인 작성분만 삭제 가능) - 통합 API 사용
|
||||||
const result = await apiCall(`${API}/daily-work-reports/my-entry/${workId}`, {
|
const result = await window.apiCall(`${window.API}/daily-work-reports/my-entry/${workId}`, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1055,8 +1055,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 스크립트 -->
|
<!-- 스크립트 -->
|
||||||
<script type="module" src="/js/load-navbar.js"></script>
|
<script src="/js/api-config.js"></script>
|
||||||
<!-- ⚠️ 중요: 모듈 타입으로 수정 -->
|
<script src="/js/load-navbar.js"></script>
|
||||||
<script type="module" src="/js/daily-work-report.js"></script>
|
<script src="/js/daily-work-report.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user