From 790d12fe131485a75c79be6c6a881f69bd85279d Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Mon, 3 Nov 2025 12:49:25 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=86=A0=ED=81=B0=20=EB=A7=8C=EB=A3=8C?= =?UTF-8?q?=20=EC=8B=9C=20=EC=9E=90=EB=8F=99=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=ED=85=8C=EC=9D=B4=EB=B8=94=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ” 토큰 만료 μ‹œ μžλ™ λ‘œκ·Έμ•„μ›ƒ κΈ°λŠ₯: 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 정상 μž‘λ™ 확인 --- api.hyungi.net/models/projectModel.js | 10 ++-- api.hyungi.net/models/taskModel.js | 10 ++-- docker-compose.yml | 2 + web-ui/js/api-config.js | 65 ++++++++++++++++++---- web-ui/js/daily-work-report.js | 26 ++++----- web-ui/pages/common/daily-work-report.html | 6 +- 6 files changed, 82 insertions(+), 37 deletions(-) diff --git a/api.hyungi.net/models/projectModel.js b/api.hyungi.net/models/projectModel.js index 1b1b976..6c3ee64 100644 --- a/api.hyungi.net/models/projectModel.js +++ b/api.hyungi.net/models/projectModel.js @@ -10,7 +10,7 @@ const create = async (project, callback) => { } = project; const [result] = await db.query( - `INSERT INTO Projects + `INSERT INTO projects (job_no, project_name, contract_date, due_date, delivery_method, site, pm) VALUES (?, ?, ?, ?, ?, ?, ?)`, [job_no, project_name, contract_date, due_date, delivery_method, site, pm] @@ -26,7 +26,7 @@ const getAll = async (callback) => { try { const db = await getDb(); const [rows] = await db.query( - `SELECT * FROM Projects ORDER BY project_id DESC` + `SELECT * FROM projects ORDER BY project_id DESC` ); callback(null, rows); } catch (err) { @@ -38,7 +38,7 @@ const getById = async (project_id, callback) => { try { const db = await getDb(); const [rows] = await db.query( - `SELECT * FROM Projects WHERE project_id = ?`, + `SELECT * FROM projects WHERE project_id = ?`, [project_id] ); callback(null, rows[0]); @@ -57,7 +57,7 @@ const update = async (project, callback) => { } = project; const [result] = await db.query( - `UPDATE Projects + `UPDATE projects SET job_no = ?, project_name = ?, contract_date = ?, @@ -79,7 +79,7 @@ const remove = async (project_id, callback) => { try { const db = await getDb(); const [result] = await db.query( - `DELETE FROM Projects WHERE project_id = ?`, + `DELETE FROM projects WHERE project_id = ?`, [project_id] ); callback(null, result.affectedRows); diff --git a/api.hyungi.net/models/taskModel.js b/api.hyungi.net/models/taskModel.js index 6ee247d..24ca845 100644 --- a/api.hyungi.net/models/taskModel.js +++ b/api.hyungi.net/models/taskModel.js @@ -7,7 +7,7 @@ const create = async (task, callback) => { const { category, subcategory, task_name, description } = task; const [result] = await db.query( - `INSERT INTO Tasks (category, subcategory, task_name, description) + `INSERT INTO tasks (category, subcategory, task_name, description) VALUES (?, ?, ?, ?)`, [category, subcategory, task_name, description] ); @@ -23,7 +23,7 @@ const getAll = async (callback) => { try { const db = await getDb(); const [rows] = await db.query( - `SELECT * FROM Tasks ORDER BY task_id DESC` + `SELECT * FROM tasks ORDER BY task_id DESC` ); callback(null, rows); } catch (err) { @@ -36,7 +36,7 @@ const getById = async (task_id, callback) => { try { const db = await getDb(); const [rows] = await db.query( - `SELECT * FROM Tasks WHERE task_id = ?`, + `SELECT * FROM tasks WHERE task_id = ?`, [task_id] ); callback(null, rows[0]); @@ -52,7 +52,7 @@ const update = async (task, callback) => { const { task_id, category, subcategory, task_name, description } = task; const [result] = await db.query( - `UPDATE Tasks + `UPDATE tasks SET category = ?, subcategory = ?, task_name = ?, @@ -72,7 +72,7 @@ const remove = async (task_id, callback) => { try { const db = await getDb(); const [result] = await db.query( - `DELETE FROM Tasks WHERE task_id = ?`, + `DELETE FROM tasks WHERE task_id = ?`, [task_id] ); callback(null, result.affectedRows); diff --git a/docker-compose.yml b/docker-compose.yml index 87a974d..dd0676a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,6 +45,8 @@ services: - DB_NAME=hyungi - DB_ROOT_PASSWORD=tkfb2024! - JWT_SECRET=tkfb_jwt_secret_2024_hyungi_secure_key + - JWT_EXPIRES_IN=7d + - JWT_REFRESH_EXPIRES_IN=30d volumes: - ./api.hyungi.net/public/img:/usr/src/app/public/img:ro - ./api.hyungi.net/uploads:/usr/src/app/uploads diff --git a/web-ui/js/api-config.js b/web-ui/js/api-config.js index 3ec1610..a6881cd 100644 --- a/web-ui/js/api-config.js +++ b/web-ui/js/api-config.js @@ -34,15 +34,45 @@ window.API_BASE_URL = API_URL; function ensureAuthenticated() { const token = localStorage.getItem('token'); - if (!token || token === 'undefined') { - alert('둜그인이 ν•„μš”ν•©λ‹ˆλ‹€'); - localStorage.removeItem('token'); - window.location.href = '/'; - return null; + if (!token || token === 'undefined' || token === 'null') { + console.log('🚨 μΈμ¦λ˜μ§€ μ•Šμ€ μ‚¬μš©μž. 둜그인 νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•©λ‹ˆλ‹€.'); + clearAuthData(); // λ§Œμ•½μ„ μœ„ν•΄ ν•œλ²ˆ 더 정리 + window.location.href = '/index.html'; + return false; // 이후 μ½”λ“œ μ‹€ν–‰ λ°©μ§€ } + + // 토큰 만료 확인 + if (isTokenExpired(token)) { + console.log('🚨 토큰이 λ§Œλ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€. 둜그인 νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•©λ‹ˆλ‹€.'); + clearAuthData(); + alert('μ„Έμ…˜μ΄ λ§Œλ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ λ‘œκ·ΈμΈν•΄μ£Όμ„Έμš”.'); + window.location.href = '/index.html'; + return false; + } + 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() { const token = localStorage.getItem('token'); return { @@ -72,11 +102,11 @@ async function apiCall(url, options = {}) { // 인증 만료 처리 if (response.status === 401) { - console.error('❌ 인증 만료'); - localStorage.removeItem('token'); - alert('인증이 λ§Œλ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ λ‘œκ·ΈμΈν•΄μ£Όμ„Έμš”.'); - window.location.href = '/'; - return; + console.error('🚨 인증 μ‹€νŒ¨: 토큰이 λ§Œλ£Œλ˜μ—ˆκ±°λ‚˜ μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.'); + clearAuthData(); + alert('μ„Έμ…˜μ΄ λ§Œλ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ λ‘œκ·ΈμΈν•΄μ£Όμ„Έμš”.'); + window.location.href = '/index.html'; + throw new Error('인증에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.'); } // 응닡 μ‹€νŒ¨ 처리 @@ -143,10 +173,23 @@ window.ensureAuthenticated = ensureAuthenticated; window.getAuthHeaders = getAuthHeaders; window.apiCall = apiCall; window.testApiConnection = testApiConnection; +window.isTokenExpired = isTokenExpired; +window.clearAuthData = clearAuthData; // 개발 λͺ¨λ“œμ—μ„œ μžλ™ ν…ŒμŠ€νŠΈ if (window.location.hostname === 'localhost' || window.location.hostname.startsWith('192.168.')) { setTimeout(() => { testApiConnection(); }, 1000); -} \ No newline at end of file +} + +// 주기적으둜 토큰 만료 확인 (5λΆ„λ§ˆλ‹€) +setInterval(() => { + const token = localStorage.getItem('token'); + if (token && isTokenExpired(token)) { + console.log('🚨 주기적 확인: 토큰이 λ§Œλ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.'); + clearAuthData(); + alert('μ„Έμ…˜μ΄ λ§Œλ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ λ‘œκ·ΈμΈν•΄μ£Όμ„Έμš”.'); + window.location.href = '/index.html'; + } +}, 5 * 60 * 1000); // 5λΆ„λ§ˆλ‹€ 확인 \ No newline at end of file diff --git a/web-ui/js/daily-work-report.js b/web-ui/js/daily-work-report.js index d8785ab..afcf762 100644 --- a/web-ui/js/daily-work-report.js +++ b/web-ui/js/daily-work-report.js @@ -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 = []; @@ -117,7 +117,7 @@ async function loadData() { async function loadWorkers() { try { 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 || []); console.log('βœ… Workers λ‘œλ“œ 성곡:', workers.length); } catch (error) { @@ -129,7 +129,7 @@ async function loadWorkers() { async function loadProjects() { try { 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 || []); console.log('βœ… Projects λ‘œλ“œ 성곡:', projects.length); } catch (error) { @@ -140,7 +140,7 @@ async function loadProjects() { async function loadWorkTypes() { 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) { workTypes = data; console.log('βœ… μž‘μ—… μœ ν˜• API μ‚¬μš© (톡합 μ„€μ •)'); @@ -159,7 +159,7 @@ async function loadWorkTypes() { async function loadWorkStatusTypes() { 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) { workStatusTypes = data; console.log('βœ… 업무 μƒνƒœ μœ ν˜• API μ‚¬μš© (톡합 μ„€μ •)'); @@ -177,7 +177,7 @@ async function loadWorkStatusTypes() { async function loadErrorTypes() { 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) { errorTypes = data; console.log('βœ… μ—λŸ¬ μœ ν˜• API μ‚¬μš© (톡합 μ„€μ •)'); @@ -424,7 +424,7 @@ async function saveWorkReport() { console.log('전솑 데이터 (톡합 API μ‚¬μš©):', requestData); try { - const result = await apiCall(`${API}/daily-work-reports`, { + const result = await window.apiCall(`${window.API}/daily-work-reports`, { method: 'POST', body: JSON.stringify(requestData) }); @@ -510,7 +510,7 @@ async function loadTodayWorkers() { 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); let data = []; @@ -645,7 +645,7 @@ async function editWorkItem(workId) { // 1. κΈ°μ‘΄ 데이터 쑰회 (톡합 API μ‚¬μš©) 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); // 2. μˆ˜μ • λͺ¨λ‹¬ ν‘œμ‹œ @@ -784,7 +784,7 @@ async function saveEditedWork() { 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', body: JSON.stringify(updateData) }); @@ -813,7 +813,7 @@ async function deleteWorkItem(workId) { showMessage('μž‘μ—…μ„ μ‚­μ œν•˜λŠ” 쀑... (톡합 API)', 'loading'); // κ°œλ³„ ν•­λͺ© μ‚­μ œ 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' }); diff --git a/web-ui/pages/common/daily-work-report.html b/web-ui/pages/common/daily-work-report.html index a8e0156..a38a914 100644 --- a/web-ui/pages/common/daily-work-report.html +++ b/web-ui/pages/common/daily-work-report.html @@ -1055,8 +1055,8 @@ - - - + + + \ No newline at end of file