feat: localStorage 문제 해결 및 시스템 개선
- localStorage와 DB ID 불일치 문제 해결 - 프로젝트별 보고서 시간 필터링 수정 - 일반 사용자에게 일일공수 메뉴 숨김 - 공통 헤더 및 인증 시스템 구현 - 프로젝트별 일일공수 분리 기능 추가 (ProjectDailyWork 모델) - IssuesAPI에서 project_id 누락 문제 수정 - 사용자 인증 통합 (TokenManager 기반)
This commit is contained in:
@@ -93,3 +93,17 @@ class DailyWork(Base):
|
|||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
created_by = relationship("User", back_populates="daily_works")
|
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")
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class LoginRequest(BaseModel):
|
|||||||
class IssueBase(BaseModel):
|
class IssueBase(BaseModel):
|
||||||
category: IssueCategory
|
category: IssueCategory
|
||||||
description: str
|
description: str
|
||||||
project_id: Optional[int] = None
|
project_id: int
|
||||||
|
|
||||||
class IssueCreate(IssueBase):
|
class IssueCreate(IssueBase):
|
||||||
photo: Optional[str] = None # Base64 encoded image
|
photo: Optional[str] = None # Base64 encoded image
|
||||||
@@ -162,3 +162,21 @@ class ReportSummary(BaseModel):
|
|||||||
category_stats: CategoryStats
|
category_stats: CategoryStats
|
||||||
completed_issues: int
|
completed_issues: int
|
||||||
average_resolution_time: float
|
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
|
||||||
|
|||||||
25
backend/migrations/009_add_project_daily_works_table.sql
Normal file
25
backend/migrations/009_add_project_daily_works_table.sql
Normal 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;
|
||||||
@@ -17,6 +17,8 @@ async def create_issue(
|
|||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
|
print(f"DEBUG: 받은 issue 데이터: {issue}")
|
||||||
|
print(f"DEBUG: project_id: {issue.project_id}")
|
||||||
# 이미지 저장
|
# 이미지 저장
|
||||||
photo_path = None
|
photo_path = None
|
||||||
photo_path2 = None
|
photo_path2 = None
|
||||||
@@ -34,6 +36,7 @@ async def create_issue(
|
|||||||
photo_path=photo_path,
|
photo_path=photo_path,
|
||||||
photo_path2=photo_path2,
|
photo_path2=photo_path2,
|
||||||
reporter_id=current_user.id,
|
reporter_id=current_user.id,
|
||||||
|
project_id=issue.project_id,
|
||||||
status=IssueStatus.new
|
status=IssueStatus.new
|
||||||
)
|
)
|
||||||
db.add(db_issue)
|
db.add(db_issue)
|
||||||
|
|||||||
@@ -196,7 +196,7 @@
|
|||||||
<nav class="bg-white border-b">
|
<nav class="bg-white border-b">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div id="navContainer" class="flex gap-2 py-2 overflow-x-auto">
|
<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>일일 공수
|
<i class="fas fa-calendar-check mr-2"></i>일일 공수
|
||||||
</a>
|
</a>
|
||||||
<button class="nav-link active" onclick="showSection('report')">
|
<button class="nav-link active" onclick="showSection('report')">
|
||||||
@@ -361,10 +361,10 @@
|
|||||||
<option value="pending">검토 필요</option>
|
<option value="pending">검토 필요</option>
|
||||||
<option value="completed">검토 완료</option>
|
<option value="completed">검토 완료</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 날짜 필터 -->
|
<!-- 날짜 필터 -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">날짜</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">날짜</label>
|
||||||
<select id="dateFilter" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" onchange="displayIssueList()">
|
<select id="dateFilter" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" onchange="displayIssueList()">
|
||||||
<option value="">전체</option>
|
<option value="">전체</option>
|
||||||
@@ -372,7 +372,7 @@
|
|||||||
<option value="week">이번 주</option>
|
<option value="week">이번 주</option>
|
||||||
<option value="month">이번 달</option>
|
<option value="month">이번 달</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 사용자 정의 날짜 -->
|
<!-- 사용자 정의 날짜 -->
|
||||||
<div>
|
<div>
|
||||||
@@ -403,9 +403,9 @@
|
|||||||
<!-- 프로젝트 옵션들이 여기에 로드됩니다 -->
|
<!-- 프로젝트 옵션들이 여기에 로드됩니다 -->
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button onclick="printReport()" class="btn-primary px-4 py-2 rounded-lg text-sm">
|
<button onclick="printReport()" class="btn-primary px-4 py-2 rounded-lg text-sm">
|
||||||
<i class="fas fa-print mr-2"></i>인쇄
|
<i class="fas fa-print mr-2"></i>인쇄
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="reportContent">
|
<div id="reportContent">
|
||||||
@@ -415,7 +415,7 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</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/image-utils.js?v=20250917"></script>
|
||||||
<script src="/static/js/date-utils.js?v=20250917"></script>
|
<script src="/static/js/date-utils.js?v=20250917"></script>
|
||||||
<script>
|
<script>
|
||||||
@@ -480,14 +480,15 @@
|
|||||||
const listBtn = document.getElementById('listBtn');
|
const listBtn = document.getElementById('listBtn');
|
||||||
const summaryBtn = document.getElementById('summaryBtn');
|
const summaryBtn = document.getElementById('summaryBtn');
|
||||||
const adminBtn = document.getElementById('adminBtn');
|
const adminBtn = document.getElementById('adminBtn');
|
||||||
|
|
||||||
const projectBtn = document.getElementById('projectBtn');
|
const projectBtn = document.getElementById('projectBtn');
|
||||||
|
const dailyWorkBtn = document.getElementById('dailyWorkBtn');
|
||||||
|
|
||||||
if (currentUser.role === 'admin') {
|
if (currentUser.role === 'admin') {
|
||||||
// 관리자는 모든 메뉴 표시
|
// 관리자는 모든 메뉴 표시
|
||||||
listBtn.style.display = '';
|
listBtn.style.display = '';
|
||||||
summaryBtn.style.display = '';
|
summaryBtn.style.display = '';
|
||||||
projectBtn.style.display = '';
|
projectBtn.style.display = '';
|
||||||
|
dailyWorkBtn.style.display = '';
|
||||||
adminBtn.style.display = '';
|
adminBtn.style.display = '';
|
||||||
adminBtn.innerHTML = '<i class="fas fa-users-cog mr-2"></i>사용자 관리';
|
adminBtn.innerHTML = '<i class="fas fa-users-cog mr-2"></i>사용자 관리';
|
||||||
} else {
|
} else {
|
||||||
@@ -495,6 +496,7 @@
|
|||||||
listBtn.style.display = 'none';
|
listBtn.style.display = 'none';
|
||||||
summaryBtn.style.display = 'none';
|
summaryBtn.style.display = 'none';
|
||||||
projectBtn.style.display = 'none';
|
projectBtn.style.display = 'none';
|
||||||
|
dailyWorkBtn.style.display = 'none';
|
||||||
adminBtn.style.display = '';
|
adminBtn.style.display = '';
|
||||||
adminBtn.innerHTML = '<i class="fas fa-key mr-2"></i>비밀번호 변경';
|
adminBtn.innerHTML = '<i class="fas fa-key mr-2"></i>비밀번호 변경';
|
||||||
}
|
}
|
||||||
@@ -811,6 +813,10 @@
|
|||||||
project_id: parseInt(projectId)
|
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();
|
const startTime = Date.now();
|
||||||
await IssuesAPI.create(issueData);
|
await IssuesAPI.create(issueData);
|
||||||
const uploadTime = Date.now() - startTime;
|
const uploadTime = Date.now() - startTime;
|
||||||
@@ -873,59 +879,92 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 프로젝트 로드
|
// 프로젝트 로드
|
||||||
function loadProjects() {
|
async function loadProjects() {
|
||||||
|
// 1. 즉시 localStorage에서 로드 (빠른 응답)
|
||||||
const saved = localStorage.getItem('work-report-projects');
|
const saved = localStorage.getItem('work-report-projects');
|
||||||
|
let projects = [];
|
||||||
|
|
||||||
if (saved) {
|
if (saved) {
|
||||||
const projects = JSON.parse(saved);
|
projects = JSON.parse(saved);
|
||||||
const activeProjects = projects.filter(p => p.isActive);
|
displayProjectsInUI(projects);
|
||||||
|
|
||||||
// 부적합 등록 폼의 프로젝트 선택 (활성 프로젝트만)
|
|
||||||
const projectSelect = document.getElementById('projectSelect');
|
|
||||||
if (projectSelect) {
|
|
||||||
projectSelect.innerHTML = '<option value="">프로젝트를 선택하세요</option>';
|
|
||||||
|
|
||||||
activeProjects.forEach(project => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = project.id;
|
|
||||||
option.textContent = `${project.jobNo} - ${project.projectName}`;
|
|
||||||
projectSelect.appendChild(option);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 목록 관리의 프로젝트 필터 (모든 프로젝트)
|
|
||||||
const listProjectFilter = document.getElementById('listProjectFilter');
|
|
||||||
if (listProjectFilter) {
|
|
||||||
listProjectFilter.innerHTML = '<option value="">전체 프로젝트</option>';
|
|
||||||
|
|
||||||
projects.forEach(project => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = project.id;
|
|
||||||
option.textContent = `${project.jobNo} - ${project.projectName}${!project.isActive ? ' (비활성)' : ''}`;
|
|
||||||
listProjectFilter.appendChild(option);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 보고서의 프로젝트 필터 (모든 프로젝트)
|
|
||||||
const reportProjectFilter = document.getElementById('reportProjectFilter');
|
|
||||||
if (reportProjectFilter) {
|
|
||||||
reportProjectFilter.innerHTML = '<option value="">전체 프로젝트</option>';
|
|
||||||
|
|
||||||
projects.forEach(project => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = project.id;
|
|
||||||
option.textContent = `${project.jobNo} - ${project.projectName}${!project.isActive ? ' (비활성)' : ''}`;
|
|
||||||
reportProjectFilter.appendChild(option);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 부적합 등록 폼의 프로젝트 선택 (활성 프로젝트만)
|
||||||
|
const projectSelect = document.getElementById('projectSelect');
|
||||||
|
if (projectSelect) {
|
||||||
|
projectSelect.innerHTML = '<option value="">프로젝트를 선택하세요</option>';
|
||||||
|
|
||||||
|
activeProjects.forEach(project => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = project.id;
|
||||||
|
option.textContent = `${project.jobNo} - ${project.projectName}`;
|
||||||
|
projectSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 목록 관리의 프로젝트 필터 (모든 프로젝트)
|
||||||
|
const listProjectFilter = document.getElementById('listProjectFilter');
|
||||||
|
if (listProjectFilter) {
|
||||||
|
listProjectFilter.innerHTML = '<option value="">전체 프로젝트</option>';
|
||||||
|
|
||||||
|
projects.forEach(project => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = project.id;
|
||||||
|
option.textContent = `${project.jobNo} - ${project.projectName}${!project.isActive ? ' (비활성)' : ''}`;
|
||||||
|
listProjectFilter.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 보고서의 프로젝트 필터 (모든 프로젝트)
|
||||||
|
const reportProjectFilter = document.getElementById('reportProjectFilter');
|
||||||
|
if (reportProjectFilter) {
|
||||||
|
reportProjectFilter.innerHTML = '<option value="">전체 프로젝트</option>';
|
||||||
|
|
||||||
|
projects.forEach(project => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = project.id;
|
||||||
|
option.textContent = `${project.jobNo} - ${project.projectName}${!project.isActive ? ' (비활성)' : ''}`;
|
||||||
|
reportProjectFilter.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 전역 캐시에 저장
|
||||||
|
window.projectsCache = projects;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 선택된 프로젝트 정보 가져오기
|
// 선택된 프로젝트 정보 가져오기
|
||||||
function getSelectedProject(projectId) {
|
function getSelectedProject(projectId) {
|
||||||
const saved = localStorage.getItem('work-report-projects');
|
if (window.projectsCache) {
|
||||||
if (saved) {
|
return window.projectsCache.find(p => p.id == projectId);
|
||||||
const projects = JSON.parse(saved);
|
|
||||||
return projects.find(p => p.id == projectId);
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -1090,7 +1129,7 @@
|
|||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- 프로젝트 정보 및 상태 (오른쪽 상단) -->
|
<!-- 프로젝트 정보 및 상태 (오른쪽 상단) -->
|
||||||
<div class="flex justify-between items-start mb-2">
|
<div class="flex justify-between items-start mb-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
${isCompleted ?
|
${isCompleted ?
|
||||||
'<div class="px-2 py-1 bg-green-100 text-green-800 rounded-full text-xs font-medium"><i class="fas fa-check-circle mr-1"></i>검토완료</div>' :
|
'<div class="px-2 py-1 bg-green-100 text-green-800 rounded-full text-xs font-medium"><i class="fas fa-check-circle mr-1"></i>검토완료</div>' :
|
||||||
@@ -1475,35 +1514,38 @@
|
|||||||
|
|
||||||
// 프로젝트별 일일 공수 데이터 계산
|
// 프로젝트별 일일 공수 데이터 계산
|
||||||
let dailyWorkTotal = 0;
|
let dailyWorkTotal = 0;
|
||||||
const dailyWorkData = JSON.parse(localStorage.getItem('daily-work-data') || '[]');
|
|
||||||
|
|
||||||
console.log('일일공수 데이터:', dailyWorkData);
|
// localStorage의 프로젝트별 데이터 우선 사용 (프로젝트별 분리 지원)
|
||||||
console.log('선택된 프로젝트 ID:', selectedProjectId);
|
const dailyWorkData = JSON.parse(localStorage.getItem('daily-work-data') || '[]');
|
||||||
|
|
||||||
if (selectedProjectId) {
|
if (selectedProjectId) {
|
||||||
// 선택된 프로젝트의 일일 공수만 합계
|
// 선택된 프로젝트의 일일 공수만 합계
|
||||||
dailyWorkData.forEach(dayWork => {
|
dailyWorkData.forEach(dayWork => {
|
||||||
console.log('일일공수 항목:', dayWork);
|
|
||||||
if (dayWork.projects) {
|
if (dayWork.projects) {
|
||||||
dayWork.projects.forEach(project => {
|
dayWork.projects.forEach(project => {
|
||||||
console.log('프로젝트:', project, '매칭 확인:', project.projectId == selectedProjectId);
|
|
||||||
if (project.projectId == selectedProjectId || project.projectId.toString() === selectedProjectId.toString()) {
|
if (project.projectId == selectedProjectId || project.projectId.toString() === selectedProjectId.toString()) {
|
||||||
dailyWorkTotal += project.hours || 0;
|
dailyWorkTotal += project.hours || 0;
|
||||||
console.log('시간 추가:', project.hours, '누적:', dailyWorkTotal);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
console.log(`프로젝트 ID ${selectedProjectId}의 총 일일공수:`, dailyWorkTotal);
|
||||||
} else {
|
} else {
|
||||||
// 전체 프로젝트의 일일 공수 합계
|
// 전체 프로젝트의 일일 공수 합계
|
||||||
dailyWorkData.forEach(dayWork => {
|
try {
|
||||||
console.log('전체 일일공수 항목:', dayWork);
|
// 백엔드 API에서 전체 일일공수 데이터 가져오기
|
||||||
dailyWorkTotal += dayWork.totalHours || 0;
|
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 => {
|
||||||
|
dailyWorkTotal += dayWork.totalHours || 0;
|
||||||
|
});
|
||||||
|
console.log('localStorage에서 가져온 전체 총 일일공수:', dailyWorkTotal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('최종 일일공수 합계:', dailyWorkTotal);
|
|
||||||
|
|
||||||
// 부적합 사항 해결 시간 계산 (필터링된 이슈만)
|
// 부적합 사항 해결 시간 계산 (필터링된 이슈만)
|
||||||
const issueHours = filteredIssues.reduce((sum, issue) => sum + (issue.work_hours || 0), 0);
|
const issueHours = filteredIssues.reduce((sum, issue) => sum + (issue.work_hours || 0), 0);
|
||||||
const categoryCount = {};
|
const categoryCount = {};
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
<nav class="bg-white border-b">
|
<nav class="bg-white border-b">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex gap-2 py-2 overflow-x-auto">
|
<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>일일 공수
|
<i class="fas fa-calendar-check mr-2"></i>일일 공수
|
||||||
</a>
|
</a>
|
||||||
<a href="index.html" class="nav-link">
|
<a href="index.html" class="nav-link">
|
||||||
@@ -187,7 +187,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- 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 src="/static/js/date-utils.js?v=20250917"></script>
|
||||||
<script>
|
<script>
|
||||||
let currentUser = null;
|
let currentUser = null;
|
||||||
@@ -607,28 +607,22 @@
|
|||||||
|
|
||||||
// 페이지 로드 시 사용자 정보 확인 및 관리자 배너 표시
|
// 페이지 로드 시 사용자 정보 확인 및 관리자 배너 표시
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
const currentUserData = localStorage.getItem('currentUser');
|
// 메인 페이지와 동일한 방식으로 토큰에서 사용자 정보 가져오기
|
||||||
if (!currentUserData) {
|
const user = TokenManager.getUser();
|
||||||
|
if (!user) {
|
||||||
window.location.href = 'index.html';
|
window.location.href = 'index.html';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentUser;
|
currentUser = user;
|
||||||
try {
|
|
||||||
// JSON 파싱 시도
|
|
||||||
currentUser = JSON.parse(currentUserData);
|
|
||||||
} catch (e) {
|
|
||||||
// JSON이 아니면 문자열로 처리 (기존 방식 호환)
|
|
||||||
currentUser = { username: currentUserData };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 사용자 표시
|
// 사용자 표시
|
||||||
const username = currentUser.username || currentUser;
|
const displayName = currentUser.full_name || currentUser.username;
|
||||||
const displayName = currentUser.full_name || (username === 'hyungi' ? '관리자' : username);
|
document.getElementById('userDisplay').textContent = `${displayName} (${currentUser.username})`;
|
||||||
document.getElementById('userDisplay').textContent = `${displayName} (${username})`;
|
|
||||||
|
|
||||||
// 관리자인 경우 메뉴 표시
|
// 관리자인 경우 메뉴 표시
|
||||||
if (username === 'hyungi' || currentUser.role === 'admin') {
|
if (currentUser.role === 'admin') {
|
||||||
|
document.getElementById('dailyWorkBtn').style.display = '';
|
||||||
document.getElementById('listBtn').style.display = '';
|
document.getElementById('listBtn').style.display = '';
|
||||||
document.getElementById('summaryBtn').style.display = '';
|
document.getElementById('summaryBtn').style.display = '';
|
||||||
document.getElementById('projectBtn').style.display = '';
|
document.getElementById('projectBtn').style.display = '';
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ const IssuesAPI = {
|
|||||||
const dataToSend = {
|
const dataToSend = {
|
||||||
category: issueData.category,
|
category: issueData.category,
|
||||||
description: issueData.description,
|
description: issueData.description,
|
||||||
|
project_id: issueData.project_id,
|
||||||
photo: issueData.photos && issueData.photos.length > 0 ? issueData.photos[0] : null,
|
photo: issueData.photos && issueData.photos.length > 0 ? issueData.photos[0] : null,
|
||||||
photo2: issueData.photos && issueData.photos.length > 1 ? issueData.photos[1] : null
|
photo2: issueData.photos && issueData.photos.length > 1 ? issueData.photos[1] : null
|
||||||
};
|
};
|
||||||
|
|||||||
68
frontend/static/js/auth-common.js
Normal file
68
frontend/static/js/auth-common.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
106
frontend/static/js/common-header.js
Normal file
106
frontend/static/js/common-header.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user