feat: localStorage 문제 해결 및 시스템 개선

- localStorage와 DB ID 불일치 문제 해결
- 프로젝트별 보고서 시간 필터링 수정
- 일반 사용자에게 일일공수 메뉴 숨김
- 공통 헤더 및 인증 시스템 구현
- 프로젝트별 일일공수 분리 기능 추가 (ProjectDailyWork 모델)
- IssuesAPI에서 project_id 누락 문제 수정
- 사용자 인증 통합 (TokenManager 기반)
This commit is contained in:
hyungi
2025-10-24 12:24:24 +09:00
parent b024a178d0
commit 5fe51ab1d5
9 changed files with 358 additions and 87 deletions

View File

@@ -93,3 +93,17 @@ class DailyWork(Base):
# Relationships
created_by = relationship("User", back_populates="daily_works")
class ProjectDailyWork(Base):
__tablename__ = "project_daily_works"
id = Column(Integer, primary_key=True, index=True)
date = Column(DateTime, nullable=False, index=True)
project_id = Column(BigInteger, ForeignKey("projects.id"), nullable=False)
hours = Column(Float, nullable=False)
created_by_id = Column(Integer, ForeignKey("users.id"))
created_at = Column(DateTime, default=get_kst_now)
# Relationships
project = relationship("Project")
created_by = relationship("User")

View File

@@ -62,7 +62,7 @@ class LoginRequest(BaseModel):
class IssueBase(BaseModel):
category: IssueCategory
description: str
project_id: Optional[int] = None
project_id: int
class IssueCreate(IssueBase):
photo: Optional[str] = None # Base64 encoded image
@@ -162,3 +162,21 @@ class ReportSummary(BaseModel):
category_stats: CategoryStats
completed_issues: int
average_resolution_time: float
# Project Daily Work schemas
class ProjectDailyWorkBase(BaseModel):
date: datetime
project_id: int
hours: float
class ProjectDailyWorkCreate(ProjectDailyWorkBase):
pass
class ProjectDailyWork(ProjectDailyWorkBase):
id: int
created_by_id: int
created_at: datetime
project: Project
class Config:
from_attributes = True

View File

@@ -0,0 +1,25 @@
-- 프로젝트별 일일공수 테이블 생성
CREATE TABLE project_daily_works (
id SERIAL PRIMARY KEY,
date DATE NOT NULL,
project_id BIGINT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
hours FLOAT NOT NULL,
created_by_id INTEGER NOT NULL REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- 인덱스 생성
CREATE INDEX idx_project_daily_works_date ON project_daily_works(date);
CREATE INDEX idx_project_daily_works_project_id ON project_daily_works(project_id);
CREATE INDEX idx_project_daily_works_date_project ON project_daily_works(date, project_id);
-- 기존 일일공수 데이터를 프로젝트별로 마이그레이션 (M Project로)
INSERT INTO project_daily_works (date, project_id, hours, created_by_id, created_at)
SELECT
date::date,
1, -- M Project ID
total_hours,
created_by_id,
created_at
FROM daily_works
WHERE total_hours > 0;

View File

@@ -17,6 +17,8 @@ async def create_issue(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
print(f"DEBUG: 받은 issue 데이터: {issue}")
print(f"DEBUG: project_id: {issue.project_id}")
# 이미지 저장
photo_path = None
photo_path2 = None
@@ -34,6 +36,7 @@ async def create_issue(
photo_path=photo_path,
photo_path2=photo_path2,
reporter_id=current_user.id,
project_id=issue.project_id,
status=IssueStatus.new
)
db.add(db_issue)

View File

@@ -196,7 +196,7 @@
<nav class="bg-white border-b">
<div class="container mx-auto px-4">
<div id="navContainer" class="flex gap-2 py-2 overflow-x-auto">
<a href="daily-work.html" class="nav-link">
<a href="daily-work.html" class="nav-link" id="dailyWorkBtn" style="display: none;">
<i class="fas fa-calendar-check mr-2"></i>일일 공수
</a>
<button class="nav-link active" onclick="showSection('report')">
@@ -415,7 +415,7 @@
</section>
</div>
<script src="/static/js/api.js?v=20250917"></script>
<script src="/static/js/api.js?v=20251024m"></script>
<script src="/static/js/image-utils.js?v=20250917"></script>
<script src="/static/js/date-utils.js?v=20250917"></script>
<script>
@@ -480,14 +480,15 @@
const listBtn = document.getElementById('listBtn');
const summaryBtn = document.getElementById('summaryBtn');
const adminBtn = document.getElementById('adminBtn');
const projectBtn = document.getElementById('projectBtn');
const dailyWorkBtn = document.getElementById('dailyWorkBtn');
if (currentUser.role === 'admin') {
// 관리자는 모든 메뉴 표시
listBtn.style.display = '';
summaryBtn.style.display = '';
projectBtn.style.display = '';
dailyWorkBtn.style.display = '';
adminBtn.style.display = '';
adminBtn.innerHTML = '<i class="fas fa-users-cog mr-2"></i>사용자 관리';
} else {
@@ -495,6 +496,7 @@
listBtn.style.display = 'none';
summaryBtn.style.display = 'none';
projectBtn.style.display = 'none';
dailyWorkBtn.style.display = 'none';
adminBtn.style.display = '';
adminBtn.innerHTML = '<i class="fas fa-key mr-2"></i>비밀번호 변경';
}
@@ -811,6 +813,10 @@
project_id: parseInt(projectId)
};
console.log('DEBUG: 전송할 issueData:', issueData);
console.log('DEBUG: projectId 원본:', projectId);
console.log('DEBUG: parseInt(projectId):', parseInt(projectId));
const startTime = Date.now();
await IssuesAPI.create(issueData);
const uploadTime = Date.now() - startTime;
@@ -873,10 +879,43 @@
}
// 프로젝트 로드
function loadProjects() {
async function loadProjects() {
// 1. 즉시 localStorage에서 로드 (빠른 응답)
const saved = localStorage.getItem('work-report-projects');
let projects = [];
if (saved) {
const projects = JSON.parse(saved);
projects = JSON.parse(saved);
displayProjectsInUI(projects);
}
// 2. 백그라운드에서 API 동기화
try {
const apiProjects = await ProjectsAPI.getAll(false);
// API 데이터를 localStorage 형식으로 변환
const syncedProjects = apiProjects.map(p => ({
id: p.id,
jobNo: p.job_no,
projectName: p.project_name,
isActive: p.is_active,
createdAt: p.created_at || new Date().toISOString(),
createdByName: '관리자'
}));
// localStorage 업데이트
localStorage.setItem('work-report-projects', JSON.stringify(syncedProjects));
// UI 다시 업데이트 (동기화된 데이터로)
displayProjectsInUI(syncedProjects);
} catch (error) {
// API 실패해도 localStorage 데이터로 계속 동작
console.log('API 동기화 실패, localStorage 데이터 사용:', error.message);
}
}
function displayProjectsInUI(projects) {
const activeProjects = projects.filter(p => p.isActive);
// 부적합 등록 폼의 프로젝트 선택 (활성 프로젝트만)
@@ -917,15 +956,15 @@
reportProjectFilter.appendChild(option);
});
}
}
// 전역 캐시에 저장
window.projectsCache = projects;
}
// 선택된 프로젝트 정보 가져오기
function getSelectedProject(projectId) {
const saved = localStorage.getItem('work-report-projects');
if (saved) {
const projects = JSON.parse(saved);
return projects.find(p => p.id == projectId);
if (window.projectsCache) {
return window.projectsCache.find(p => p.id == projectId);
}
return null;
}
@@ -1475,34 +1514,37 @@
// 프로젝트별 일일 공수 데이터 계산
let dailyWorkTotal = 0;
const dailyWorkData = JSON.parse(localStorage.getItem('daily-work-data') || '[]');
console.log('일일공수 데이터:', dailyWorkData);
console.log('선택된 프로젝트 ID:', selectedProjectId);
// localStorage의 프로젝트별 데이터 우선 사용 (프로젝트별 분리 지원)
const dailyWorkData = JSON.parse(localStorage.getItem('daily-work-data') || '[]');
if (selectedProjectId) {
// 선택된 프로젝트의 일일 공수만 합계
dailyWorkData.forEach(dayWork => {
console.log('일일공수 항목:', dayWork);
if (dayWork.projects) {
dayWork.projects.forEach(project => {
console.log('프로젝트:', project, '매칭 확인:', project.projectId == selectedProjectId);
if (project.projectId == selectedProjectId || project.projectId.toString() === selectedProjectId.toString()) {
dailyWorkTotal += project.hours || 0;
console.log('시간 추가:', project.hours, '누적:', dailyWorkTotal);
}
});
}
});
console.log(`프로젝트 ID ${selectedProjectId}의 총 일일공수:`, dailyWorkTotal);
} else {
// 전체 프로젝트의 일일 공수 합계
try {
// 백엔드 API에서 전체 일일공수 데이터 가져오기
const apiDailyWork = await DailyWorkAPI.getAll();
dailyWorkTotal = apiDailyWork.reduce((sum, work) => sum + (work.total_hours || 0), 0);
console.log('API에서 가져온 전체 총 일일공수:', dailyWorkTotal);
} catch (error) {
// API 실패 시 localStorage 사용
dailyWorkData.forEach(dayWork => {
console.log('전체 일일공수 항목:', dayWork);
dailyWorkTotal += dayWork.totalHours || 0;
});
console.log('localStorage에서 가져온 전체 총 일일공수:', dailyWorkTotal);
}
}
console.log('최종 일일공수 합계:', dailyWorkTotal);
// 부적합 사항 해결 시간 계산 (필터링된 이슈만)
const issueHours = filteredIssues.reduce((sum, issue) => sum + (issue.work_hours || 0), 0);

View File

@@ -88,7 +88,7 @@
<nav class="bg-white border-b">
<div class="container mx-auto px-4">
<div class="flex gap-2 py-2 overflow-x-auto">
<a href="daily-work.html" class="nav-link">
<a href="daily-work.html" class="nav-link" id="dailyWorkBtn" style="display: none;">
<i class="fas fa-calendar-check mr-2"></i>일일 공수
</a>
<a href="index.html" class="nav-link">
@@ -187,7 +187,7 @@
</main>
<!-- Scripts -->
<script src="/static/js/api.js?v=20250917"></script>
<script src="/static/js/api.js?v=20251024m"></script>
<script src="/static/js/date-utils.js?v=20250917"></script>
<script>
let currentUser = null;
@@ -607,28 +607,22 @@
// 페이지 로드 시 사용자 정보 확인 및 관리자 배너 표시
window.addEventListener('DOMContentLoaded', () => {
const currentUserData = localStorage.getItem('currentUser');
if (!currentUserData) {
// 메인 페이지와 동일한 방식으로 토큰에서 사용자 정보 가져오기
const user = TokenManager.getUser();
if (!user) {
window.location.href = 'index.html';
return;
}
let currentUser;
try {
// JSON 파싱 시도
currentUser = JSON.parse(currentUserData);
} catch (e) {
// JSON이 아니면 문자열로 처리 (기존 방식 호환)
currentUser = { username: currentUserData };
}
currentUser = user;
// 사용자 표시
const username = currentUser.username || currentUser;
const displayName = currentUser.full_name || (username === 'hyungi' ? '관리자' : username);
document.getElementById('userDisplay').textContent = `${displayName} (${username})`;
const displayName = currentUser.full_name || currentUser.username;
document.getElementById('userDisplay').textContent = `${displayName} (${currentUser.username})`;
// 관리자인 경우 메뉴 표시
if (username === 'hyungi' || currentUser.role === 'admin') {
if (currentUser.role === 'admin') {
document.getElementById('dailyWorkBtn').style.display = '';
document.getElementById('listBtn').style.display = '';
document.getElementById('summaryBtn').style.display = '';
document.getElementById('projectBtn').style.display = '';

View File

@@ -135,6 +135,7 @@ const IssuesAPI = {
const dataToSend = {
category: issueData.category,
description: issueData.description,
project_id: issueData.project_id,
photo: issueData.photos && issueData.photos.length > 0 ? issueData.photos[0] : null,
photo2: issueData.photos && issueData.photos.length > 1 ? issueData.photos[1] : null
};

View File

@@ -0,0 +1,68 @@
// 공통 인증 및 네비게이션 관리
class AuthCommon {
static init(currentPage = '') {
// 토큰 기반 사용자 정보 확인
const user = TokenManager.getUser();
if (!user) {
window.location.href = 'index.html';
return null;
}
// 전역 currentUser 설정
window.currentUser = user;
// 헤더 생성 (페이지별로 다른 active 상태)
CommonHeader.init(currentPage);
// 사용자 정보 표시
this.updateUserDisplay(user);
// 네비게이션 권한 업데이트
this.updateNavigation(user);
return user;
}
static updateUserDisplay(user) {
const userDisplayElement = document.getElementById('userDisplay');
if (userDisplayElement) {
const displayName = user.full_name || user.username;
userDisplayElement.textContent = `${displayName} (${user.username})`;
}
}
static updateNavigation(user) {
const isAdmin = user.role === 'admin';
// 관리자 전용 메뉴들
const adminMenus = [
'dailyWorkBtn',
'listBtn',
'summaryBtn',
'projectBtn',
'adminBtn'
];
adminMenus.forEach(menuId => {
const element = document.getElementById(menuId);
if (element) {
element.style.display = isAdmin ? '' : 'none';
}
});
}
static logout() {
AuthAPI.logout();
}
}
// 전역 함수들
function logout() {
AuthCommon.logout();
}
function showSection(sectionName) {
if (typeof window.showSection === 'function') {
window.showSection(sectionName);
}
}

View File

@@ -0,0 +1,106 @@
// 공통 헤더 생성 및 관리
class CommonHeader {
static create(currentPage = '') {
return `
<!-- 헤더 -->
<header class="bg-white shadow-sm sticky top-0 z-50">
<div class="container mx-auto px-4 py-3">
<div class="flex justify-between items-center">
<h1 class="text-xl font-bold text-gray-800">
<i class="fas fa-clipboard-check text-blue-500 mr-2"></i>작업보고서
</h1>
<div class="flex items-center gap-4">
<span class="text-sm text-gray-600" id="userDisplay"></span>
<button onclick="AuthCommon.logout()" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-sign-out-alt"></i>
</button>
</div>
</div>
</div>
</header>
<!-- 네비게이션 -->
<nav class="bg-white border-b">
<div class="container mx-auto px-4">
<div class="flex gap-2 py-2 overflow-x-auto">
<a href="daily-work.html" class="nav-link" id="dailyWorkBtn" style="display: none;">
<i class="fas fa-calendar-check mr-2"></i>일일 공수
</a>
${this.getNavButton('index.html', 'mainBtn', 'fas fa-camera-retro', '부적합 등록', currentPage === 'main')}
${this.getNavButton('issue-view.html', 'issueViewBtn', 'fas fa-search', '부적합 조회', currentPage === 'issue-view')}
${this.getNavButtonInternal('list', 'listBtn', 'fas fa-list', '목록 관리', currentPage === 'list')}
${this.getNavButtonInternal('summary', 'summaryBtn', 'fas fa-chart-bar', '보고서', currentPage === 'summary')}
<a href="project-management.html" class="nav-link" style="display:none;" id="projectBtn">
<i class="fas fa-folder-open mr-2"></i>프로젝트 관리
</a>
<a href="admin.html" class="nav-link" style="display:none;" id="adminBtn">
<i class="fas fa-users-cog mr-2"></i>관리
</a>
</div>
</div>
</nav>
`;
}
static getNavButton(href, id, iconClass, text, isActive = false) {
const activeClass = isActive ? ' active' : '';
return `<a href="${href}" class="nav-link${activeClass}" id="${id}">
<i class="${iconClass} mr-2"></i>${text}
</a>`;
}
static getNavButtonInternal(section, id, iconClass, text, isActive = false) {
const activeClass = isActive ? ' active' : '';
if (section === 'list' || section === 'summary') {
return `<button class="nav-link${activeClass}" onclick="showSection('${section}')" style="display:none;" id="${id}">
<i class="${iconClass} mr-2"></i>${text}
</button>`;
}
return `<a href="index.html#${section}" class="nav-link${activeClass}" style="display:none;" id="${id}">
<i class="${iconClass} mr-2"></i>${text}
</a>`;
}
static init(currentPage = '') {
// 헤더 HTML 삽입
const headerContainer = document.getElementById('header-container');
if (headerContainer) {
headerContainer.innerHTML = this.create();
}
// 현재 페이지 활성화
this.setActivePage(currentPage);
}
static setActivePage(currentPage) {
// 모든 nav-link에서 active 클래스 제거
document.querySelectorAll('.nav-link').forEach(link => {
link.classList.remove('active');
});
// 현재 페이지에 active 클래스 추가
const activeElement = document.getElementById(currentPage);
if (activeElement) {
activeElement.classList.add('active');
}
}
}
// 관리자 버튼 클릭 처리 (전역 함수)
function handleAdminClick() {
if (window.currentUser && window.currentUser.role === 'admin') {
window.location.href = 'admin.html';
} else {
// 비밀번호 변경 모달 표시
if (typeof showPasswordChangeModal === 'function') {
showPasswordChangeModal();
}
}
}
// 섹션 전환 (메인 페이지용)
function showSection(sectionName) {
if (typeof window.showSection === 'function') {
window.showSection(sectionName);
}
}