Fix: 페이지 간 이동 시 로그아웃 문제 해결 및 기능 개선
- 토큰 저장 키 통일 (access_token으로 일관성 확보) - 일일공수 페이지 API 스크립트 로딩 순서 수정 - 프로젝트 관리 페이지 비활성 프로젝트 표시 문제 해결 - 업로드 카테고리에 '기타' 항목 추가 (백엔드 schemas.py 포함) - 비밀번호 변경 기능 API 연동으로 수정 - 프로젝트 드롭다운 z-index 문제 해결 - CORS 설정 및 Nginx 구성 개선 - 비밀번호 해싱 방식 pbkdf2_sha256으로 변경 (bcrypt 72바이트 제한 해결)
This commit is contained in:
@@ -122,85 +122,176 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- API 스크립트 먼저 로드 (최강 캐시 무력화) -->
|
||||
<script>
|
||||
// 브라우저 캐시 완전 무력화
|
||||
const timestamp = new Date().getTime();
|
||||
const random1 = Math.random() * 1000000;
|
||||
const random2 = Math.floor(Math.random() * 1000000);
|
||||
const cacheBuster = `${timestamp}-${random1}-${random2}`;
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = `/static/js/api.js?force-reload=${cacheBuster}&no-cache=${timestamp}&bust=${random2}`;
|
||||
script.onload = function() {
|
||||
console.log('✅ API 스크립트 로드 완료');
|
||||
console.log('🔍 API_BASE_URL:', typeof API_BASE_URL !== 'undefined' ? API_BASE_URL : 'undefined');
|
||||
console.log('🌐 현재 hostname:', window.location.hostname);
|
||||
console.log('🔗 현재 protocol:', window.location.protocol);
|
||||
// API 로드 후 인증 체크 시작
|
||||
setTimeout(checkAdminAccess, 100);
|
||||
};
|
||||
script.setAttribute('cache-control', 'no-cache, no-store, must-revalidate');
|
||||
script.setAttribute('pragma', 'no-cache');
|
||||
script.setAttribute('expires', '0');
|
||||
document.head.appendChild(script);
|
||||
|
||||
console.log('🚀 캐시 버스터:', cacheBuster);
|
||||
</script>
|
||||
|
||||
<!-- API 스크립트 동적 로딩 -->
|
||||
<script>
|
||||
// 최강 캐시 무력화로 API 스크립트 로드
|
||||
const timestamp = new Date().getTime();
|
||||
const random1 = Math.random() * 1000000;
|
||||
const random2 = Math.floor(Math.random() * 1000000);
|
||||
const cacheBuster = `${timestamp}-${random1}-${random2}`;
|
||||
|
||||
// 기존 api.js 스크립트 제거
|
||||
const existingScripts = document.querySelectorAll('script[src*="api.js"]');
|
||||
existingScripts.forEach(script => script.remove());
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = `/static/js/api.js?v=${timestamp}&cb=${random1}&bust=${random2}&force=${Date.now()}`;
|
||||
script.onload = function() {
|
||||
console.log('✅ API 스크립트 로드 완료 (project-management.html)');
|
||||
console.log('🔍 API_BASE_URL:', typeof API_BASE_URL !== 'undefined' ? API_BASE_URL : 'undefined');
|
||||
// API 로드 후 인증 확인 시작
|
||||
checkAdminAccess();
|
||||
};
|
||||
script.setAttribute('cache-control', 'no-cache, no-store, must-revalidate');
|
||||
script.setAttribute('pragma', 'no-cache');
|
||||
script.setAttribute('expires', '0');
|
||||
script.crossOrigin = 'anonymous';
|
||||
document.head.appendChild(script);
|
||||
|
||||
console.log('📱 프로젝트 관리 - 캐시 버스터:', cacheBuster);
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// 사용자 확인 (관리자만 접근 가능)
|
||||
const currentUserData = localStorage.getItem('currentUser');
|
||||
if (!currentUserData) {
|
||||
alert('로그인이 필요합니다.');
|
||||
window.location.href = 'index.html';
|
||||
let currentUser = null;
|
||||
|
||||
async function initAuth() {
|
||||
console.log('인증 초기화 시작');
|
||||
const token = localStorage.getItem('access_token');
|
||||
console.log('토큰 존재:', !!token);
|
||||
|
||||
if (!token) {
|
||||
console.log('토큰 없음 - 로그인 페이지로 이동');
|
||||
alert('로그인이 필요합니다.');
|
||||
window.location.href = 'index.html';
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('API로 사용자 정보 가져오는 중...');
|
||||
const user = await AuthAPI.getCurrentUser();
|
||||
console.log('사용자 정보:', user);
|
||||
currentUser = user;
|
||||
localStorage.setItem('currentUser', JSON.stringify(user));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('인증 실패:', error);
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('currentUser');
|
||||
alert('로그인이 필요합니다.');
|
||||
window.location.href = 'index.html';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let currentUser;
|
||||
try {
|
||||
// JSON 파싱 시도
|
||||
currentUser = JSON.parse(currentUserData);
|
||||
} catch (e) {
|
||||
// JSON이 아니면 문자열로 처리 (기존 방식 호환)
|
||||
currentUser = { username: currentUserData };
|
||||
async function checkAdminAccess() {
|
||||
const authSuccess = await initAuth();
|
||||
if (!authSuccess) return;
|
||||
|
||||
const username = currentUser.username || currentUser;
|
||||
const isAdmin = username === 'hyungi' || currentUser.role === 'admin';
|
||||
|
||||
if (!isAdmin) {
|
||||
alert('관리자만 접근 가능합니다.');
|
||||
window.location.href = 'index.html';
|
||||
return;
|
||||
}
|
||||
|
||||
const displayName = currentUser.full_name || (username === 'hyungi' ? '관리자' : username);
|
||||
document.getElementById('userDisplay').textContent = `${displayName} (${username})`;
|
||||
|
||||
// 프로젝트 로드
|
||||
loadProjects();
|
||||
}
|
||||
|
||||
const username = currentUser.username || currentUser;
|
||||
const isAdmin = username === 'hyungi' || currentUser.role === 'admin';
|
||||
|
||||
if (!isAdmin) {
|
||||
alert('관리자만 접근 가능합니다.');
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
|
||||
const displayName = currentUser.full_name || (username === 'hyungi' ? '관리자' : username);
|
||||
document.getElementById('userDisplay').textContent = `${displayName} (${username})`;
|
||||
|
||||
let projects = [];
|
||||
|
||||
// 프로젝트 데이터 로드
|
||||
function loadProjects() {
|
||||
const saved = localStorage.getItem('work-report-projects');
|
||||
if (saved) {
|
||||
projects = JSON.parse(saved);
|
||||
// 프로젝트 데이터 로드 (API 기반)
|
||||
async function loadProjects() {
|
||||
console.log('프로젝트 로드 시작 (API)');
|
||||
|
||||
try {
|
||||
// API에서 프로젝트 로드
|
||||
const apiProjects = await ProjectsAPI.getAll(false);
|
||||
|
||||
// API 데이터를 그대로 사용 (필드명 통일)
|
||||
projects = apiProjects;
|
||||
|
||||
console.log('API에서 프로젝트 로드:', projects.length, '개');
|
||||
|
||||
} catch (error) {
|
||||
console.error('API 로드 실패:', error);
|
||||
projects = [];
|
||||
}
|
||||
|
||||
displayProjectList();
|
||||
}
|
||||
|
||||
// 프로젝트 데이터 저장
|
||||
function saveProjects() {
|
||||
localStorage.setItem('work-report-projects', JSON.stringify(projects));
|
||||
}
|
||||
// 프로젝트 데이터 저장 (더 이상 사용하지 않음 - API 기반)
|
||||
// function saveProjects() {
|
||||
// localStorage.setItem('work-report-projects', JSON.stringify(projects));
|
||||
// }
|
||||
|
||||
// 프로젝트 생성 폼 처리
|
||||
document.getElementById('projectForm').addEventListener('submit', (e) => {
|
||||
document.getElementById('projectForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const jobNo = document.getElementById('jobNo').value.trim();
|
||||
const projectName = document.getElementById('projectName').value.trim();
|
||||
|
||||
// 중복 Job No. 확인
|
||||
if (projects.some(p => p.jobNo === jobNo)) {
|
||||
if (projects.some(p => p.job_no === jobNo)) {
|
||||
alert('이미 존재하는 Job No.입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 프로젝트 생성
|
||||
const newProject = {
|
||||
id: Date.now(),
|
||||
jobNo: jobNo,
|
||||
projectName: projectName,
|
||||
createdBy: 'hyungi',
|
||||
createdByName: '관리자',
|
||||
createdAt: new Date().toISOString(),
|
||||
isActive: true
|
||||
};
|
||||
|
||||
projects.push(newProject);
|
||||
saveProjects();
|
||||
|
||||
// 성공 메시지
|
||||
alert('프로젝트가 생성되었습니다.');
|
||||
|
||||
// 폼 초기화
|
||||
document.getElementById('projectForm').reset();
|
||||
|
||||
// 목록 새로고침
|
||||
displayProjectList();
|
||||
try {
|
||||
// API를 통한 프로젝트 생성
|
||||
const newProject = await ProjectsAPI.create({
|
||||
job_no: jobNo,
|
||||
project_name: projectName
|
||||
});
|
||||
|
||||
// 성공 메시지
|
||||
alert('프로젝트가 생성되었습니다.');
|
||||
|
||||
// 폼 초기화
|
||||
document.getElementById('projectForm').reset();
|
||||
|
||||
// 목록 새로고침
|
||||
await loadProjects();
|
||||
displayProjectList();
|
||||
|
||||
} catch (error) {
|
||||
console.error('프로젝트 생성 실패:', error);
|
||||
alert('프로젝트 생성에 실패했습니다: ' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 프로젝트 목록 표시
|
||||
@@ -214,8 +305,13 @@
|
||||
}
|
||||
|
||||
// 활성 프로젝트와 비활성 프로젝트 분리
|
||||
const activeProjects = projects.filter(p => p.isActive);
|
||||
const inactiveProjects = projects.filter(p => !p.isActive);
|
||||
const activeProjects = projects.filter(p => p.is_active);
|
||||
const inactiveProjects = projects.filter(p => !p.is_active);
|
||||
|
||||
console.log('전체 프로젝트:', projects.length, '개');
|
||||
console.log('활성 프로젝트:', activeProjects.length, '개');
|
||||
console.log('비활성 프로젝트:', inactiveProjects.length, '개');
|
||||
console.log('비활성 프로젝트 목록:', inactiveProjects);
|
||||
|
||||
// 활성 프로젝트 표시
|
||||
if (activeProjects.length > 0) {
|
||||
@@ -231,13 +327,13 @@
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<h3 class="font-semibold text-gray-800">${project.jobNo}</h3>
|
||||
<h3 class="font-semibold text-gray-800">${project.job_no}</h3>
|
||||
<span class="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">활성</span>
|
||||
</div>
|
||||
<p class="text-gray-600 mb-2">${project.projectName}</p>
|
||||
<p class="text-gray-600 mb-2">${project.project_name}</p>
|
||||
<div class="flex items-center gap-4 text-sm text-gray-500">
|
||||
<span><i class="fas fa-user mr-1"></i>${project.createdByName}</span>
|
||||
<span><i class="fas fa-calendar mr-1"></i>${new Date(project.createdAt).toLocaleDateString()}</span>
|
||||
<span><i class="fas fa-user mr-1"></i>${project.created_by?.full_name || '관리자'}</span>
|
||||
<span><i class="fas fa-calendar mr-1"></i>${new Date(project.created_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@@ -271,13 +367,13 @@
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<h3 class="font-semibold text-gray-600">${project.jobNo}</h3>
|
||||
<h3 class="font-semibold text-gray-600">${project.job_no}</h3>
|
||||
<span class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-full">비활성</span>
|
||||
</div>
|
||||
<p class="text-gray-500 mb-2">${project.projectName}</p>
|
||||
<p class="text-gray-500 mb-2">${project.project_name}</p>
|
||||
<div class="flex items-center gap-4 text-sm text-gray-400">
|
||||
<span><i class="fas fa-user mr-1"></i>${project.createdByName}</span>
|
||||
<span><i class="fas fa-calendar mr-1"></i>${new Date(project.createdAt).toLocaleDateString()}</span>
|
||||
<span><i class="fas fa-user mr-1"></i>${project.created_by?.full_name || '관리자'}</span>
|
||||
<span><i class="fas fa-calendar mr-1"></i>${new Date(project.created_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@@ -296,55 +392,82 @@
|
||||
}
|
||||
|
||||
// 프로젝트 편집
|
||||
function editProject(projectId) {
|
||||
async function editProject(projectId) {
|
||||
const project = projects.find(p => p.id === projectId);
|
||||
if (!project) return;
|
||||
|
||||
const newName = prompt('프로젝트 이름을 수정하세요:', project.projectName);
|
||||
if (newName && newName.trim() && newName.trim() !== project.projectName) {
|
||||
project.projectName = newName.trim();
|
||||
saveProjects();
|
||||
displayProjectList();
|
||||
alert('프로젝트가 수정되었습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트 활성/비활성 토글
|
||||
function toggleProjectStatus(projectId) {
|
||||
const project = projects.find(p => p.id === projectId);
|
||||
if (!project) return;
|
||||
|
||||
const action = project.isActive ? '비활성화' : '활성화';
|
||||
if (confirm(`"${project.jobNo}" 프로젝트를 ${action}하시겠습니까?`)) {
|
||||
project.isActive = !project.isActive;
|
||||
saveProjects();
|
||||
displayProjectList();
|
||||
alert(`프로젝트가 ${action}되었습니다.`);
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트 삭제 (완전 삭제)
|
||||
function deleteProject(projectId) {
|
||||
const project = projects.find(p => p.id === projectId);
|
||||
if (!project) return;
|
||||
|
||||
const confirmMessage = project.isActive
|
||||
? `"${project.jobNo}" 프로젝트를 완전히 삭제하시겠습니까?\n\n※ 활성 프로젝트입니다. 먼저 비활성화를 권장합니다.`
|
||||
: `"${project.jobNo}" 프로젝트를 완전히 삭제하시겠습니까?\n\n※ 이 작업은 되돌릴 수 없습니다.`;
|
||||
|
||||
if (confirm(confirmMessage)) {
|
||||
const index = projects.findIndex(p => p.id === projectId);
|
||||
if (index > -1) {
|
||||
projects.splice(index, 1);
|
||||
saveProjects();
|
||||
const newName = prompt('프로젝트 이름을 수정하세요:', project.project_name);
|
||||
if (newName && newName.trim() && newName.trim() !== project.project_name) {
|
||||
try {
|
||||
// API를 통한 프로젝트 업데이트
|
||||
await ProjectsAPI.update(projectId, {
|
||||
project_name: newName.trim()
|
||||
});
|
||||
|
||||
// 목록 새로고침
|
||||
await loadProjects();
|
||||
displayProjectList();
|
||||
alert('프로젝트가 완전히 삭제되었습니다.');
|
||||
alert('프로젝트가 수정되었습니다.');
|
||||
|
||||
} catch (error) {
|
||||
console.error('프로젝트 수정 실패:', error);
|
||||
alert('프로젝트 수정에 실패했습니다: ' + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지 로드 시 프로젝트 목록 로드
|
||||
loadProjects();
|
||||
// 프로젝트 활성/비활성 토글
|
||||
async function toggleProjectStatus(projectId) {
|
||||
const project = projects.find(p => p.id === projectId);
|
||||
if (!project) return;
|
||||
|
||||
const action = project.is_active ? '비활성화' : '활성화';
|
||||
if (confirm(`"${project.job_no}" 프로젝트를 ${action}하시겠습니까?`)) {
|
||||
try {
|
||||
// API를 통한 프로젝트 상태 업데이트
|
||||
await ProjectsAPI.update(projectId, {
|
||||
is_active: !project.is_active
|
||||
});
|
||||
|
||||
// 목록 새로고침
|
||||
await loadProjects();
|
||||
displayProjectList();
|
||||
alert(`프로젝트가 ${action}되었습니다.`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('프로젝트 상태 변경 실패:', error);
|
||||
alert('프로젝트 상태 변경에 실패했습니다: ' + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트 삭제 (완전 삭제)
|
||||
async function deleteProject(projectId) {
|
||||
const project = projects.find(p => p.id === projectId);
|
||||
if (!project) return;
|
||||
|
||||
const confirmMessage = project.is_active
|
||||
? `"${project.job_no}" 프로젝트를 완전히 삭제하시겠습니까?\n\n※ 활성 프로젝트입니다. 먼저 비활성화를 권장합니다.`
|
||||
: `"${project.job_no}" 프로젝트를 완전히 삭제하시겠습니까?\n\n※ 이 작업은 되돌릴 수 없습니다.`;
|
||||
|
||||
if (confirm(confirmMessage)) {
|
||||
try {
|
||||
// API를 통한 프로젝트 삭제
|
||||
await ProjectsAPI.delete(projectId);
|
||||
|
||||
// 목록 새로고침
|
||||
await loadProjects();
|
||||
displayProjectList();
|
||||
alert('프로젝트가 완전히 삭제되었습니다.');
|
||||
|
||||
} catch (error) {
|
||||
console.error('프로젝트 삭제 실패:', error);
|
||||
alert('프로젝트 삭제에 실패했습니다: ' + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DOMContentLoaded 이벤트 제거 - API 스크립트 로드 후 checkAdminAccess() 호출됨
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user