🐛 Fix: Project.name → project_name 속성명 수정 및 보고서 시스템 안정화

- backend/routers/reports.py: project.name을 project.project_name으로 수정 (3곳)
- 일일보고서 엑셀 내보내기 오류 해결
- 배포 가이드 업데이트 (DEPLOYMENT_GUIDE_20251028.md)
- 프로젝트 속성명 불일치로 인한 500 에러 해결

Fixes: 'Project' object has no attribute 'name' 오류
This commit is contained in:
Hyungi Ahn
2025-10-28 16:36:56 +09:00
parent 5aad973028
commit 58156da987
31 changed files with 1380 additions and 16 deletions

View File

@@ -0,0 +1,240 @@
# 배포 가이드 - 2025.10.28 업데이트
## 📋 **변경사항 요약**
### 🎯 **주요 기능 개선**
- **보고서 시스템** 구현 (일일/주간/월간 보고서)
- **품질팀용 일일보고서 엑셀 내보내기** 기능
- **Project 모델 속성명 수정** (name → project_name)
- **API URL 절대 경로 사용** 개선
---
## 🗄️ **데이터베이스 변경사항**
### **⚠️ 중요: 스키마 불일치 수정**
- `Project` 모델에서 `name` 속성이 `project_name`으로 변경됨
- 백엔드 코드에서 `project.name``project.project_name` 수정 필요
### **새로운 의존성**
```txt
# backend/requirements.txt에 추가됨
openpyxl==3.1.2 # 엑셀 파일 생성용
```
---
## 🔧 **백엔드 변경사항**
### **1. 스키마 업데이트**
- `backend/database/schemas.py`
- `DailyReportRequest` 클래스 추가
- `DailyReportStats` 클래스 추가
### **2. API 엔드포인트 추가**
- `backend/routers/reports.py`
- `POST /api/reports/daily-export` 엔드포인트 추가
- `backend/routers/management.py`
- `GET /api/management/stats` 엔드포인트 추가
### **3. 중요 수정사항**
- `backend/routers/reports.py`에서 `project.name``project.project_name` 수정 (3곳)
---
## 🎨 **프론트엔드 변경사항**
### **1. 새로운 페이지 추가**
- `frontend/reports.html` - 보고서 메인 페이지
- `frontend/reports-daily.html` - 일일보고서 생성 페이지
- `frontend/reports-weekly.html` - 주간보고서 페이지 (준비중)
- `frontend/reports-monthly.html` - 월간보고서 페이지 (준비중)
### **2. 공통 헤더 개선**
- `frontend/static/js/components/common-header.js`
- 보고서 서브메뉴 추가 (일일/주간/월간)
### **3. API URL 절대 경로 수정**
- 모든 프론트엔드 파일에서 `window.API_BASE_URL` 사용
- 상대 경로 `/api/` → 절대 경로 `http://localhost:16080/api` 변경
### **4. 캐시 무효화**
- `frontend/sw.js` 버전 업데이트 (`v1.0.0``v1.0.1`)
---
## 🚀 **배포 절차**
### **1. 사전 준비**
```bash
# 1. 현재 데이터베이스 백업
docker-compose exec postgres pg_dump -U postgres -d m_project > backup_$(date +%Y%m%d_%H%M%S).sql
# 2. Git 최신 코드 pull
git pull origin master
```
### **2. 백엔드 배포**
```bash
# 1. Docker 컨테이너 중지
docker-compose down
# 2. 이미지 재빌드 (새로운 의존성 포함)
docker-compose build backend
# 3. 컨테이너 시작
docker-compose up -d
# 4. 백엔드 로그 확인
docker-compose logs backend --tail=20
```
### **3. 프론트엔드 배포**
```bash
# 1. Nginx 재시작 (캐시 무효화)
docker-compose restart nginx
# 2. 브라우저 캐시 강제 새로고침 안내
# 사용자들에게 Ctrl+Shift+F5 또는 Cmd+Shift+R 안내
```
### **4. 배포 후 검증**
```bash
# 1. 백엔드 상태 확인
docker-compose logs backend --tail=20
# 2. 새로운 API 엔드포인트 테스트
curl -X GET "http://localhost:16080/api/management/stats?project_id=1" \
-H "Authorization: Bearer YOUR_TOKEN"
# 3. 엑셀 내보내기 테스트
curl -X POST "http://localhost:16080/api/reports/daily-export" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"project_id": 1}' \
--output test_report.xlsx
```
---
## ✅ **기능 테스트 체크리스트**
### **보고서 시스템 테스트**
- [ ] 보고서 메인 페이지 접근 확인
- [ ] 일일보고서 페이지 로드 확인
- [ ] 프로젝트 목록 정상 표시 확인
- [ ] 프로젝트 선택 시 통계 표시 확인
- [ ] 엑셀 파일 다운로드 정상 작동 확인
- [ ] 엑셀 파일 내용 및 형식 확인
### **기존 기능 회귀 테스트**
- [ ] 대시보드 정상 로드 확인
- [ ] 관리함 정상 작동 확인
- [ ] 수신함 정상 작동 확인
- [ ] 일일 공수 정상 작동 확인
---
## 🔍 **트러블슈팅**
### **일반적인 문제들**
#### **1. Project.name 속성 오류**
```bash
# 증상: "Project object has no attribute 'name'" 오류
# 원인: 백엔드에서 project.name 사용
# 해결: 백엔드 재시작 후 확인
docker-compose restart backend
docker-compose logs backend --tail=20
```
#### **2. 프로젝트 목록 표시 안됨**
```bash
# 증상: 드롭다운에 프로젝트가 나타나지 않음
# 원인: API 경로 문제 또는 캐시 문제
# 해결:
# 1. 브라우저 강제 새로고침 (Ctrl+Shift+F5)
# 2. 개발자 도구에서 네트워크 탭 확인
# 3. API 호출 URL이 올바른지 확인
```
#### **3. 엑셀 파일 생성 실패**
```bash
# 증상: 500 Internal Server Error
# 원인: openpyxl 모듈 누락
# 해결: 백엔드 이미지 재빌드
docker-compose build backend
docker-compose up -d backend
```
#### **4. 공통 헤더 로드 실패**
```bash
# 증상: TypeError: window.commonHeader.init is not a function
# 원인: 스크립트 로드 순서 문제
# 해결: 페이지 새로고침 또는 캐시 클리어
```
#### **5. API 연결 실패 (CORS 오류)**
```bash
# 증상: "Fetch API cannot load http://localhost/api/"
# 원인: API URL 설정 문제
# 해결:
# 1. 브라우저 캐시 클리어
# 2. 서비스 워커 캐시 무효화 확인
# 3. nginx 재시작
docker-compose restart nginx
```
---
## 📊 **성능 모니터링**
### **모니터링 포인트**
```bash
# 1. 백엔드 메모리 사용량 확인
docker stats m-project-backend
# 2. 데이터베이스 연결 상태 확인
docker-compose exec postgres psql -U postgres -d m_project -c "SELECT count(*) FROM pg_stat_activity;"
# 3. 엑셀 생성 성능 확인 (대용량 데이터)
time curl -X POST "http://localhost:16080/api/reports/daily-export" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"project_id": 1}' \
--output performance_test.xlsx
```
---
## 📞 **지원 연락처**
- 개발자: [개발자 연락처]
- 배포 담당자: [배포 담당자 연락처]
---
**⚠️ 주의사항:**
1. **반드시 데이터베이스 백업 후 배포 진행**
2. **백엔드 이미지 재빌드 필수** (새로운 의존성 포함)
3. **브라우저 캐시 무효화 안내** 필요
4. **Project.name → project_name 수정사항 확인**
5. **배포 후 보고서 기능 전체 테스트 필수**
---
## 🔄 **롤백 절차**
문제 발생 시 다음 순서로 롤백:
```bash
# 1. 이전 버전으로 코드 롤백
git checkout [이전_커밋_해시]
# 2. 백엔드 이미지 재빌드
docker-compose build backend
# 3. 컨테이너 재시작
docker-compose up -d
# 4. 데이터베이스 롤백 (필요시)
# 백업 파일에서 복원
```

View File

@@ -324,3 +324,13 @@ class ProjectDailyWork(ProjectDailyWorkBase):
class Config:
from_attributes = True
# 일일보고서 관련 스키마
class DailyReportRequest(BaseModel):
project_id: int
class DailyReportStats(BaseModel):
total_count: int = 0
management_count: int = 0 # 관리처리 현황 (진행 중)
completed_count: int = 0
delayed_count: int = 0

View File

@@ -11,3 +11,4 @@ pydantic-settings==2.1.0
pillow==10.1.0
pillow-heif==0.13.0
reportlab==4.0.7
openpyxl==3.1.2

View File

@@ -6,7 +6,7 @@ from typing import List
from database.database import get_db
from database.models import Issue, User, ReviewStatus
from database.schemas import (
ManagementUpdateRequest, AdditionalInfoUpdateRequest, Issue as IssueSchema
ManagementUpdateRequest, AdditionalInfoUpdateRequest, Issue as IssueSchema, DailyReportStats
)
from routers.auth import get_current_user
from routers.page_permissions import check_page_access
@@ -172,3 +172,41 @@ async def get_additional_info(
"additional_info_updated_at": issue.additional_info_updated_at,
"additional_info_updated_by_id": issue.additional_info_updated_by_id
}
@router.get("/stats", response_model=DailyReportStats)
async def get_management_stats(
project_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""
프로젝트별 관리함 통계 조회 (보고서용)
"""
# 관리함 페이지 권한 확인
if not check_page_access(current_user.id, 'issues_management', db):
raise HTTPException(status_code=403, detail="관리함 접근 권한이 없습니다.")
# 해당 프로젝트의 관리함 이슈들 조회
issues = db.query(Issue).filter(
Issue.project_id == project_id,
Issue.review_status.in_([ReviewStatus.in_progress, ReviewStatus.completed])
).all()
# 통계 계산
stats = DailyReportStats()
today = datetime.now().date()
for issue in issues:
stats.total_count += 1
if issue.review_status == ReviewStatus.in_progress:
stats.management_count += 1
# 지연 여부 확인
if issue.expected_completion_date and issue.expected_completion_date < today:
stats.delayed_count += 1
elif issue.review_status == ReviewStatus.completed:
stats.completed_count += 1
return stats

View File

@@ -1,11 +1,16 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from sqlalchemy import func
from datetime import datetime
from sqlalchemy import func, and_, or_
from datetime import datetime, date
from typing import List
import io
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from database.database import get_db
from database.models import Issue, DailyWork, IssueStatus, IssueCategory, User, UserRole
from database.models import Issue, DailyWork, IssueStatus, IssueCategory, User, UserRole, Project, ReviewStatus
from database import schemas
from routers.auth import get_current_user
@@ -128,3 +133,222 @@ async def get_report_daily_works(
"overtime_total": work.overtime_total,
"total_hours": work.total_hours
} for work in works]
@router.post("/daily-export")
async def export_daily_report(
request: schemas.DailyReportRequest,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""품질팀용 일일보고서 엑셀 내보내기"""
# 권한 확인 (품질팀만 접근 가능)
if current_user.role != UserRole.admin:
raise HTTPException(status_code=403, detail="품질팀만 접근 가능합니다")
# 프로젝트 확인
project = db.query(Project).filter(Project.id == request.project_id).first()
if not project:
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다")
# 관리함 데이터 조회 (진행 중 + 완료됨)
issues_query = db.query(Issue).filter(
Issue.project_id == request.project_id,
Issue.review_status.in_([ReviewStatus.in_progress, ReviewStatus.completed])
).order_by(Issue.report_date.desc())
issues = issues_query.all()
# 통계 계산
stats = calculate_project_stats(issues)
# 엑셀 파일 생성
wb = Workbook()
ws = wb.active
ws.title = "일일보고서"
# 스타일 정의
header_font = Font(bold=True, color="FFFFFF")
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
stats_font = Font(bold=True, size=12)
stats_fill = PatternFill(start_color="E7E6E6", end_color="E7E6E6", fill_type="solid")
border = Border(
left=Side(style='thin'),
right=Side(style='thin'),
top=Side(style='thin'),
bottom=Side(style='thin')
)
center_alignment = Alignment(horizontal='center', vertical='center')
# 제목 및 기본 정보
ws.merge_cells('A1:L1')
ws['A1'] = f"{project.project_name} - 일일보고서"
ws['A1'].font = Font(bold=True, size=16)
ws['A1'].alignment = center_alignment
ws.merge_cells('A2:L2')
ws['A2'] = f"생성일: {datetime.now().strftime('%Y년 %m월 %d')}"
ws['A2'].alignment = center_alignment
# 프로젝트 통계 (4행부터)
ws.merge_cells('A4:L4')
ws['A4'] = "프로젝트 현황"
ws['A4'].font = stats_font
ws['A4'].fill = stats_fill
ws['A4'].alignment = center_alignment
# 통계 데이터
stats_row = 5
ws[f'A{stats_row}'] = "총 신고 수량"
ws[f'B{stats_row}'] = stats.total_count
ws[f'D{stats_row}'] = "관리처리 현황"
ws[f'E{stats_row}'] = stats.management_count
ws[f'G{stats_row}'] = "완료 현황"
ws[f'H{stats_row}'] = stats.completed_count
ws[f'J{stats_row}'] = "지연 중"
ws[f'K{stats_row}'] = stats.delayed_count
# 통계 스타일 적용
for col in ['A', 'D', 'G', 'J']:
ws[f'{col}{stats_row}'].font = Font(bold=True)
ws[f'{col}{stats_row}'].fill = PatternFill(start_color="FFF2CC", end_color="FFF2CC", fill_type="solid")
# 데이터 테이블 헤더 (7행부터)
headers = [
"번호", "프로젝트", "부적합명", "상세내용", "원인분류",
"해결방안", "담당부서", "담당자", "마감일", "상태",
"신고일", "완료일"
]
header_row = 7
for col, header in enumerate(headers, 1):
cell = ws.cell(row=header_row, column=col, value=header)
cell.font = header_font
cell.fill = header_fill
cell.alignment = center_alignment
cell.border = border
# 데이터 입력
current_row = header_row + 1
for issue in issues:
# 완료됨 항목의 첫 내보내기 여부 확인 (실제로는 DB에 플래그를 저장해야 함)
# 지금은 모든 완료됨 항목을 포함
ws.cell(row=current_row, column=1, value=issue.id)
ws.cell(row=current_row, column=2, value=project.project_name)
ws.cell(row=current_row, column=3, value=issue.nonconformity_name or "")
ws.cell(row=current_row, column=4, value=issue.detail_notes or "")
ws.cell(row=current_row, column=5, value=get_category_text(issue.category))
ws.cell(row=current_row, column=6, value=issue.solution or "")
ws.cell(row=current_row, column=7, value=get_department_text(issue.responsible_department))
ws.cell(row=current_row, column=8, value=issue.responsible_person or "")
ws.cell(row=current_row, column=9, value=issue.expected_completion_date.strftime('%Y-%m-%d') if issue.expected_completion_date else "")
ws.cell(row=current_row, column=10, value=get_status_text(issue.review_status))
ws.cell(row=current_row, column=11, value=issue.report_date.strftime('%Y-%m-%d') if issue.report_date else "")
ws.cell(row=current_row, column=12, value=issue.completion_date.strftime('%Y-%m-%d') if issue.completion_date else "")
# 상태별 색상 적용
status_color = get_status_color(issue.review_status)
if status_color:
for col in range(1, len(headers) + 1):
ws.cell(row=current_row, column=col).fill = PatternFill(
start_color=status_color, end_color=status_color, fill_type="solid"
)
# 테두리 적용
for col in range(1, len(headers) + 1):
ws.cell(row=current_row, column=col).border = border
ws.cell(row=current_row, column=col).alignment = Alignment(vertical='center')
current_row += 1
# 열 너비 자동 조정
for col in range(1, len(headers) + 1):
column_letter = get_column_letter(col)
ws.column_dimensions[column_letter].width = 15
# 특정 열 너비 조정
ws.column_dimensions['C'].width = 20 # 부적합명
ws.column_dimensions['D'].width = 30 # 상세내용
ws.column_dimensions['F'].width = 25 # 해결방안
# 엑셀 파일을 메모리에 저장
excel_buffer = io.BytesIO()
wb.save(excel_buffer)
excel_buffer.seek(0)
# 파일명 생성
today = date.today().strftime('%Y%m%d')
filename = f"{project.project_name}_일일보고서_{today}.xlsx"
return StreamingResponse(
io.BytesIO(excel_buffer.read()),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
def calculate_project_stats(issues: List[Issue]) -> schemas.DailyReportStats:
"""프로젝트 통계 계산"""
stats = schemas.DailyReportStats()
today = date.today()
for issue in issues:
stats.total_count += 1
if issue.review_status == ReviewStatus.in_progress:
stats.management_count += 1
# 지연 여부 확인
if issue.expected_completion_date and issue.expected_completion_date < today:
stats.delayed_count += 1
elif issue.review_status == ReviewStatus.completed:
stats.completed_count += 1
return stats
def get_category_text(category: IssueCategory) -> str:
"""카테고리 한글 변환"""
category_map = {
IssueCategory.material_missing: "자재 누락",
IssueCategory.design_error: "설계 미스",
IssueCategory.incoming_defect: "입고 불량",
IssueCategory.inspection_miss: "검사 미스",
IssueCategory.etc: "기타"
}
return category_map.get(category, str(category))
def get_department_text(department) -> str:
"""부서 한글 변환"""
if not department:
return ""
department_map = {
"production": "생산",
"quality": "품질",
"purchasing": "구매",
"design": "설계",
"sales": "영업"
}
return department_map.get(department, str(department))
def get_status_text(status: ReviewStatus) -> str:
"""상태 한글 변환"""
status_map = {
ReviewStatus.pending_review: "검토 대기",
ReviewStatus.in_progress: "진행 중",
ReviewStatus.completed: "완료됨",
ReviewStatus.disposed: "폐기됨"
}
return status_map.get(status, str(status))
def get_status_color(status: ReviewStatus) -> str:
"""상태별 색상 반환"""
color_map = {
ReviewStatus.in_progress: "FFF2CC", # 연한 노랑
ReviewStatus.completed: "E2EFDA", # 연한 초록
ReviewStatus.disposed: "F2F2F2" # 연한 회색
}
return color_map.get(status, None)

View File

@@ -339,7 +339,8 @@
async function loadProjects() {
try {
// API에서 최신 프로젝트 데이터 가져오기
const response = await fetch('/api/projects/', {
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
const response = await fetch(`${apiUrl}/projects/`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'

View File

@@ -287,7 +287,8 @@
// 프로젝트 로드
async function loadProjects() {
try {
const response = await fetch('/api/projects/', {
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
const response = await fetch(`${apiUrl}/projects/`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'

View File

@@ -323,7 +323,8 @@
// 데이터 로드 함수들
async function loadProjects() {
try {
const response = await fetch('/api/projects/', {
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
const response = await fetch(`${apiUrl}/projects/`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'

View File

@@ -668,7 +668,8 @@
async function loadProjects() {
console.log('🔄 프로젝트 로드 시작');
try {
const response = await fetch('/api/projects/', {
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
const response = await fetch(`${apiUrl}/projects/`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'

View File

@@ -465,7 +465,8 @@
// 프로젝트 로드
async function loadProjects() {
try {
const response = await fetch('/api/projects/', {
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
const response = await fetch(`${apiUrl}/projects/`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'
@@ -2548,5 +2549,6 @@
</div>
</div>
</div>
</body>
</html>

383
frontend/reports-daily.html Normal file
View File

@@ -0,0 +1,383 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>일일보고서 - 작업보고서</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Custom Styles -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
}
.report-card {
transition: all 0.2s ease;
border-left: 4px solid transparent;
}
.report-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border-left-color: #10b981;
}
.stats-card {
transition: all 0.2s ease;
}
.stats-card:hover {
transform: translateY(-1px);
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- Main Content -->
<main class="container mx-auto px-4 py-8" style="padding-top: 80px;">
<!-- 페이지 헤더 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
<div class="flex items-center justify-between mb-4">
<div>
<h1 class="text-2xl font-bold text-gray-900 flex items-center">
<i class="fas fa-file-excel text-green-500 mr-3"></i>
일일보고서
</h1>
<p class="text-gray-600 mt-1">품질팀용 관리함 데이터를 엑셀 형태로 내보내세요</p>
</div>
</div>
</div>
<!-- 프로젝트 선택 및 생성 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
<div class="space-y-6">
<!-- 프로젝트 선택 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-3">
<i class="fas fa-folder text-blue-500 mr-2"></i>보고서 생성할 프로젝트 선택
</label>
<select id="reportProjectSelect" class="w-full max-w-md px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 text-lg">
<option value="">프로젝트를 선택하세요</option>
</select>
<p class="text-sm text-gray-500 mt-2">
<i class="fas fa-info-circle mr-1"></i>
선택한 프로젝트의 관리함 데이터만 포함됩니다.
</p>
</div>
<!-- 생성 버튼 -->
<div class="flex items-center space-x-4">
<button id="generateReportBtn"
onclick="generateDailyReport()"
class="px-6 py-3 bg-green-600 text-white font-medium rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
disabled>
<i class="fas fa-download mr-2"></i>일일보고서 생성
</button>
<button id="previewStatsBtn"
onclick="toggleStatsPreview()"
class="px-6 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors hidden">
<i class="fas fa-chart-bar mr-2"></i>통계 미리보기
</button>
</div>
</div>
</div>
<!-- 프로젝트 통계 미리보기 -->
<div id="projectStatsCard" class="bg-white rounded-xl shadow-sm p-6 mb-6 hidden">
<h2 class="text-xl font-semibold text-gray-900 mb-4">
<i class="fas fa-chart-bar text-blue-500 mr-2"></i>프로젝트 현황 미리보기
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="stats-card bg-blue-50 p-4 rounded-lg text-center">
<div class="text-3xl font-bold text-blue-600 mb-1" id="reportTotalCount">0</div>
<div class="text-sm text-blue-700 font-medium">총 신고 수량</div>
</div>
<div class="stats-card bg-orange-50 p-4 rounded-lg text-center">
<div class="text-3xl font-bold text-orange-600 mb-1" id="reportManagementCount">0</div>
<div class="text-sm text-orange-700 font-medium">관리처리 현황</div>
</div>
<div class="stats-card bg-green-50 p-4 rounded-lg text-center">
<div class="text-3xl font-bold text-green-600 mb-1" id="reportCompletedCount">0</div>
<div class="text-sm text-green-700 font-medium">완료 현황</div>
</div>
<div class="stats-card bg-red-50 p-4 rounded-lg text-center">
<div class="text-3xl font-bold text-red-600 mb-1" id="reportDelayedCount">0</div>
<div class="text-sm text-red-700 font-medium">지연 중</div>
</div>
</div>
</div>
<!-- 포함 항목 안내 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
<h2 class="text-xl font-semibold text-gray-900 mb-4">
<i class="fas fa-list-check text-gray-500 mr-2"></i>보고서 포함 항목
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="report-card bg-blue-50 p-4 rounded-lg">
<div class="flex items-center mb-2">
<i class="fas fa-check-circle text-blue-500 mr-2"></i>
<span class="font-medium text-blue-800">진행 중 항목</span>
</div>
<p class="text-sm text-blue-600">무조건 포함됩니다</p>
</div>
<div class="report-card bg-green-50 p-4 rounded-lg">
<div class="flex items-center mb-2">
<i class="fas fa-check-circle text-green-500 mr-2"></i>
<span class="font-medium text-green-800">완료됨 항목</span>
</div>
<p class="text-sm text-green-600">첫 내보내기에만 포함, 이후 자동 제외</p>
</div>
<div class="report-card bg-yellow-50 p-4 rounded-lg">
<div class="flex items-center mb-2">
<i class="fas fa-info-circle text-yellow-500 mr-2"></i>
<span class="font-medium text-yellow-800">프로젝트 통계</span>
</div>
<p class="text-sm text-yellow-600">상단에 요약 정보 포함</p>
</div>
</div>
</div>
<!-- 최근 생성된 보고서 -->
<div class="bg-white rounded-xl shadow-sm p-6">
<h2 class="text-xl font-semibold text-gray-900 mb-4">
<i class="fas fa-history text-gray-500 mr-2"></i>최근 생성된 일일보고서
</h2>
<div id="recentReports" class="space-y-3">
<div class="text-center py-8 text-gray-500">
<i class="fas fa-file-excel text-4xl mb-3 opacity-50"></i>
<p>아직 생성된 일일보고서가 없습니다.</p>
<p class="text-sm">프로젝트를 선택하고 보고서를 생성해보세요!</p>
</div>
</div>
</div>
</main>
<!-- JavaScript -->
<script src="/static/js/core/auth-manager.js"></script>
<script src="/static/js/core/permissions.js"></script>
<script src="/static/js/components/common-header.js"></script>
<script src="/static/js/api.js"></script>
<script>
let projects = [];
let selectedProjectId = null;
// 페이지 초기화
document.addEventListener('DOMContentLoaded', function() {
console.log('일일보고서 페이지 로드 시작');
// AuthManager 로드 대기
const checkAuthManager = async () => {
if (window.authManager) {
try {
// 인증 확인
const isAuthenticated = await window.authManager.checkAuth();
if (!isAuthenticated) {
window.location.href = '/login.html';
return;
}
// 프로젝트 목록 로드
await loadProjects();
// 공통 헤더 초기화
try {
const user = JSON.parse(localStorage.getItem('currentUser') || '{}');
if (window.commonHeader && user.id) {
await window.commonHeader.init(user, 'reports_daily');
}
} catch (headerError) {
console.error('공통 헤더 초기화 오류:', headerError);
}
console.log('일일보고서 페이지 로드 완료');
} catch (error) {
console.error('페이지 초기화 오류:', error);
}
} else {
setTimeout(checkAuthManager, 100);
}
};
checkAuthManager();
});
// 프로젝트 목록 로드
async function loadProjects() {
try {
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
const response = await fetch(`${apiUrl}/projects/`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
}
});
if (response.ok) {
projects = await response.json();
populateProjectSelect();
} else {
console.error('프로젝트 로드 실패:', response.status);
}
} catch (error) {
console.error('프로젝트 로드 오류:', error);
}
}
// 프로젝트 선택 옵션 채우기
function populateProjectSelect() {
const select = document.getElementById('reportProjectSelect');
if (!select) {
console.error('reportProjectSelect 요소를 찾을 수 없습니다!');
return;
}
select.innerHTML = '<option value="">프로젝트를 선택하세요</option>';
projects.forEach(project => {
const option = document.createElement('option');
option.value = project.id;
option.textContent = project.project_name || project.name;
select.appendChild(option);
});
}
// 프로젝트 선택 시 이벤트
document.addEventListener('change', async function(e) {
if (e.target.id === 'reportProjectSelect') {
selectedProjectId = e.target.value;
const generateBtn = document.getElementById('generateReportBtn');
const previewBtn = document.getElementById('previewStatsBtn');
if (selectedProjectId) {
generateBtn.disabled = false;
previewBtn.classList.remove('hidden');
await loadProjectStats(selectedProjectId);
} else {
generateBtn.disabled = true;
previewBtn.classList.add('hidden');
document.getElementById('projectStatsCard').classList.add('hidden');
}
}
});
// 프로젝트 통계 로드
async function loadProjectStats(projectId) {
try {
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
const response = await fetch(`${apiUrl}/management/stats?project_id=${projectId}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
}
});
if (response.ok) {
const stats = await response.json();
document.getElementById('reportTotalCount').textContent = stats.total_count || 0;
document.getElementById('reportManagementCount').textContent = stats.management_count || 0;
document.getElementById('reportCompletedCount').textContent = stats.completed_count || 0;
document.getElementById('reportDelayedCount').textContent = stats.delayed_count || 0;
} else {
console.error('프로젝트 통계 로드 실패:', response.status);
}
} catch (error) {
console.error('프로젝트 통계 로드 오류:', error);
}
}
// 통계 미리보기 토글
function toggleStatsPreview() {
const statsCard = document.getElementById('projectStatsCard');
statsCard.classList.toggle('hidden');
}
// 일일보고서 생성
async function generateDailyReport() {
if (!selectedProjectId) {
alert('프로젝트를 선택해주세요.');
return;
}
try {
const button = document.getElementById('generateReportBtn');
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>생성 중...';
button.disabled = true;
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
const response = await fetch(`${apiUrl}/reports/daily-export`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
},
body: JSON.stringify({
project_id: parseInt(selectedProjectId)
})
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
// 파일명 생성 (프로젝트명_일일보고서_날짜.xlsx)
const project = projects.find(p => p.id == selectedProjectId);
const today = new Date().toISOString().split('T')[0];
a.download = `${project.name}_일일보고서_${today}.xlsx`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
// 성공 메시지 표시
showSuccessMessage('일일보고서가 성공적으로 생성되었습니다!');
} else {
const error = await response.text();
console.error('보고서 생성 실패:', error);
alert('보고서 생성에 실패했습니다. 다시 시도해주세요.');
}
} catch (error) {
console.error('보고서 생성 오류:', error);
alert('보고서 생성 중 오류가 발생했습니다.');
} finally {
const button = document.getElementById('generateReportBtn');
button.innerHTML = '<i class="fas fa-download mr-2"></i>일일보고서 생성';
button.disabled = false;
}
}
// 성공 메시지 표시
function showSuccessMessage(message) {
const successDiv = document.createElement('div');
successDiv.className = 'fixed top-4 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg z-50';
successDiv.innerHTML = `
<div class="flex items-center">
<i class="fas fa-check-circle mr-2"></i>
<span>${message}</span>
</div>
`;
document.body.appendChild(successDiv);
setTimeout(() => {
successDiv.remove();
}, 3000);
}
</script>
</body>
</html>

View File

@@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>월간보고서 - 작업보고서</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Custom Styles -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- Main Content -->
<main class="container mx-auto px-4 py-8" style="padding-top: 80px;">
<!-- 페이지 헤더 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
<div class="flex items-center justify-between mb-4">
<div>
<h1 class="text-2xl font-bold text-gray-900 flex items-center">
<i class="fas fa-calendar-alt text-purple-500 mr-3"></i>
월간보고서
</h1>
<p class="text-gray-600 mt-1">월간 부적합 발생 현황, 처리 성과 및 개선사항을 종합적으로 분석하세요</p>
</div>
</div>
</div>
<!-- 준비중 안내 -->
<div class="bg-white rounded-xl shadow-sm p-12 text-center">
<div class="max-w-md mx-auto">
<div class="w-24 h-24 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-6">
<i class="fas fa-calendar-alt text-purple-500 text-3xl"></i>
</div>
<h2 class="text-2xl font-bold text-gray-900 mb-4">월간보고서 준비중</h2>
<p class="text-gray-600 mb-6">
월간 부적합 발생 현황, 처리 성과 및 개선사항을 종합한 보고서 기능을 준비하고 있습니다.
</p>
<div class="bg-purple-50 p-4 rounded-lg">
<h3 class="font-semibold text-purple-800 mb-2">예정 기능</h3>
<ul class="text-sm text-purple-700 space-y-1">
<li>• 월간 부적합 발생 현황</li>
<li>• 월간 처리 완료 현황</li>
<li>• 부서별 성과 분석</li>
<li>• 월간 트렌드 및 개선사항</li>
<li>• 경영진 보고용 요약</li>
</ul>
</div>
<div class="mt-6">
<button onclick="window.history.back()"
class="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">
<i class="fas fa-arrow-left mr-2"></i>이전 페이지로
</button>
</div>
</div>
</div>
</main>
<!-- JavaScript -->
<script src="/static/js/core/auth-manager.js"></script>
<script src="/static/js/core/permissions.js"></script>
<script src="/static/js/components/common-header.js"></script>
<script src="/static/js/api.js"></script>
<script>
// 페이지 초기화
document.addEventListener('DOMContentLoaded', function() {
console.log('월간보고서 페이지 로드 시작');
// AuthManager 로드 대기
const checkAuthManager = async () => {
if (window.authManager) {
try {
// 인증 확인
const isAuthenticated = await window.authManager.checkAuth();
if (!isAuthenticated) {
window.location.href = '/login.html';
return;
}
// 공통 헤더 초기화
const user = JSON.parse(localStorage.getItem('currentUser') || '{}');
if (window.commonHeader && user.id) {
await window.commonHeader.init(user, 'reports_monthly');
}
console.log('월간보고서 페이지 로드 완료');
} catch (error) {
console.error('페이지 초기화 오류:', error);
}
} else {
setTimeout(checkAuthManager, 100);
}
};
checkAuthManager();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>주간보고서 - 작업보고서</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Custom Styles -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- Main Content -->
<main class="container mx-auto px-4 py-8" style="padding-top: 80px;">
<!-- 페이지 헤더 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
<div class="flex items-center justify-between mb-4">
<div>
<h1 class="text-2xl font-bold text-gray-900 flex items-center">
<i class="fas fa-calendar-week text-blue-500 mr-3"></i>
주간보고서
</h1>
<p class="text-gray-600 mt-1">주간 단위로 집계된 부적합 현황 및 처리 결과를 확인하세요</p>
</div>
</div>
</div>
<!-- 준비중 안내 -->
<div class="bg-white rounded-xl shadow-sm p-12 text-center">
<div class="max-w-md mx-auto">
<div class="w-24 h-24 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-6">
<i class="fas fa-calendar-week text-blue-500 text-3xl"></i>
</div>
<h2 class="text-2xl font-bold text-gray-900 mb-4">주간보고서 준비중</h2>
<p class="text-gray-600 mb-6">
주간 단위로 집계된 부적합 현황 및 처리 결과를 정리한 보고서 기능을 준비하고 있습니다.
</p>
<div class="bg-blue-50 p-4 rounded-lg">
<h3 class="font-semibold text-blue-800 mb-2">예정 기능</h3>
<ul class="text-sm text-blue-700 space-y-1">
<li>• 주간 부적합 발생 현황</li>
<li>• 주간 처리 완료 현황</li>
<li>• 부서별 처리 성과</li>
<li>• 주간 트렌드 분석</li>
</ul>
</div>
<div class="mt-6">
<button onclick="window.history.back()"
class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
<i class="fas fa-arrow-left mr-2"></i>이전 페이지로
</button>
</div>
</div>
</div>
</main>
<!-- JavaScript -->
<script src="/static/js/core/auth-manager.js"></script>
<script src="/static/js/core/permissions.js"></script>
<script src="/static/js/components/common-header.js"></script>
<script src="/static/js/api.js"></script>
<script>
// 페이지 초기화
document.addEventListener('DOMContentLoaded', function() {
console.log('주간보고서 페이지 로드 시작');
// AuthManager 로드 대기
const checkAuthManager = async () => {
if (window.authManager) {
try {
// 인증 확인
const isAuthenticated = await window.authManager.checkAuth();
if (!isAuthenticated) {
window.location.href = '/login.html';
return;
}
// 공통 헤더 초기화
const user = JSON.parse(localStorage.getItem('currentUser') || '{}');
if (window.commonHeader && user.id) {
await window.commonHeader.init(user, 'reports_weekly');
}
console.log('주간보고서 페이지 로드 완료');
} catch (error) {
console.error('페이지 초기화 오류:', error);
}
} else {
setTimeout(checkAuthManager, 100);
}
};
checkAuthManager();
});
</script>
</body>
</html>

212
frontend/reports.html Normal file
View File

@@ -0,0 +1,212 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>보고서 - 작업보고서</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Custom Styles -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
}
.report-card {
transition: all 0.3s ease;
border-left: 4px solid transparent;
}
.report-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15);
border-left-color: #3b82f6;
}
.report-card.daily-report {
border-left-color: #10b981;
}
.report-card.daily-report:hover {
border-left-color: #059669;
}
.gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.stats-card {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
</style>
</head>
<body class="bg-gray-50">
<!-- 공통 헤더 -->
<div id="commonHeader"></div>
<!-- Main Content -->
<main class="container mx-auto px-4 py-8" style="padding-top: 80px;">
<!-- 페이지 헤더 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
<div class="flex items-center justify-between mb-4">
<div>
<h1 class="text-2xl font-bold text-gray-900 flex items-center">
<i class="fas fa-chart-bar text-red-500 mr-3"></i>
보고서
</h1>
<p class="text-gray-600 mt-1">다양한 보고서를 생성하고 관리할 수 있습니다</p>
</div>
</div>
</div>
<!-- 보고서 카테고리 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
<h2 class="text-xl font-semibold text-gray-900 mb-4">
<i class="fas fa-list text-gray-500 mr-2"></i>보고서 유형 선택
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- 일일보고서 -->
<a href="/reports-daily.html" class="report-card bg-green-50 p-4 rounded-lg hover:bg-green-100 transition-colors">
<div class="flex items-center justify-between mb-3">
<div class="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
<i class="fas fa-file-excel text-green-600"></i>
</div>
<span class="bg-green-100 text-green-800 text-xs font-medium px-2 py-1 rounded-full">
사용 가능
</span>
</div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">일일보고서</h3>
<p class="text-sm text-gray-600 mb-3">
관리함 데이터를 기반으로 품질팀용 일일보고서를 엑셀 형태로 생성합니다.
</p>
<div class="flex items-center justify-between">
<span class="text-xs text-green-600 font-medium">
<i class="fas fa-check-circle mr-1"></i>진행중 항목 포함
</span>
<i class="fas fa-arrow-right text-gray-400"></i>
</div>
</a>
<!-- 주간보고서 -->
<a href="/reports-weekly.html" class="report-card bg-blue-50 p-4 rounded-lg hover:bg-blue-100 transition-colors">
<div class="flex items-center justify-between mb-3">
<div class="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<i class="fas fa-calendar-week text-blue-600"></i>
</div>
<span class="bg-yellow-100 text-yellow-800 text-xs font-medium px-2 py-1 rounded-full">
준비중
</span>
</div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">주간보고서</h3>
<p class="text-sm text-gray-600 mb-3">
주간 단위로 집계된 부적합 현황 및 처리 결과를 정리한 보고서입니다.
</p>
<div class="flex items-center justify-between">
<span class="text-xs text-blue-600 font-medium">
<i class="fas fa-calendar mr-1"></i>주간 집계
</span>
<i class="fas fa-arrow-right text-gray-400"></i>
</div>
</a>
<!-- 월간보고서 -->
<a href="/reports-monthly.html" class="report-card bg-purple-50 p-4 rounded-lg hover:bg-purple-100 transition-colors">
<div class="flex items-center justify-between mb-3">
<div class="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
<i class="fas fa-calendar-alt text-purple-600"></i>
</div>
<span class="bg-yellow-100 text-yellow-800 text-xs font-medium px-2 py-1 rounded-full">
준비중
</span>
</div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">월간보고서</h3>
<p class="text-sm text-gray-600 mb-3">
월간 부적합 발생 현황, 처리 성과 및 개선사항을 종합한 보고서입니다.
</p>
<div class="flex items-center justify-between">
<span class="text-xs text-purple-600 font-medium">
<i class="fas fa-chart-line mr-1"></i>월간 분석
</span>
<i class="fas fa-arrow-right text-gray-400"></i>
</div>
</a>
</div>
</div>
<!-- 보고서 안내 -->
<div class="bg-white rounded-xl shadow-sm p-6">
<h2 class="text-xl font-semibold text-gray-900 mb-4">
<i class="fas fa-info-circle text-blue-500 mr-2"></i>보고서 이용 안내
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-3">
<h3 class="font-semibold text-gray-800">📊 일일보고서</h3>
<ul class="text-sm text-gray-600 space-y-1">
<li>• 관리함의 진행 중 항목 무조건 포함</li>
<li>• 완료됨 항목은 첫 내보내기에만 포함</li>
<li>• 프로젝트별 개별 생성</li>
<li>• 엑셀 형태로 다운로드</li>
</ul>
</div>
<div class="space-y-3">
<h3 class="font-semibold text-gray-800">🚀 향후 계획</h3>
<ul class="text-sm text-gray-600 space-y-1">
<li>• 주간보고서: 주간 집계 및 트렌드 분석</li>
<li>• 월간보고서: 월간 성과 및 개선사항</li>
<li>• 자동 이메일 발송 기능</li>
<li>• 대시보드 형태의 실시간 리포트</li>
</ul>
</div>
</div>
</div>
</main>
<!-- JavaScript -->
<script src="/static/js/core/auth-manager.js"></script>
<script src="/static/js/core/permissions.js"></script>
<script src="/static/js/components/common-header.js"></script>
<script src="/static/js/api.js"></script>
<script>
// 페이지 초기화
document.addEventListener('DOMContentLoaded', function() {
console.log('보고서 메인 페이지 로드 시작');
// AuthManager 로드 대기
const checkAuthManager = async () => {
if (window.authManager) {
try {
// 인증 확인
const isAuthenticated = await window.authManager.checkAuth();
if (!isAuthenticated) {
window.location.href = '/login.html';
return;
}
// 공통 헤더 초기화
const user = JSON.parse(localStorage.getItem('currentUser') || '{}');
if (window.commonHeader && user.id) {
await window.commonHeader.init(user, 'reports');
}
console.log('보고서 메인 페이지 로드 완료');
} catch (error) {
console.error('페이지 초기화 오류:', error);
}
} else {
setTimeout(checkAuthManager, 100);
}
};
checkAuthManager();
});
</script>
</body>
</html>

View File

@@ -93,7 +93,33 @@ class CommonHeader {
url: '/reports.html',
pageName: 'reports',
color: 'text-red-600',
bgColor: 'bg-red-50 hover:bg-red-100'
bgColor: 'bg-red-50 hover:bg-red-100',
subMenus: [
{
id: 'reports_daily',
title: '일일보고서',
icon: 'fas fa-file-excel',
url: '/reports-daily.html',
pageName: 'reports_daily',
color: 'text-green-600'
},
{
id: 'reports_weekly',
title: '주간보고서',
icon: 'fas fa-calendar-week',
url: '/reports-weekly.html',
pageName: 'reports_weekly',
color: 'text-blue-600'
},
{
id: 'reports_monthly',
title: '월간보고서',
icon: 'fas fa-calendar-alt',
url: '/reports-monthly.html',
pageName: 'reports_monthly',
color: 'text-purple-600'
}
]
},
{
id: 'projects_manage',

View File

@@ -46,7 +46,8 @@ class PagePermissionManager {
try {
// API에서 사용자별 페이지 권한 가져오기
const response = await fetch(`/api/users/${this.currentUser.id}/page-permissions`, {
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
const response = await fetch(`${apiUrl}/users/${this.currentUser.id}/page-permissions`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
}
@@ -198,7 +199,8 @@ class PagePermissionManager {
}
try {
const response = await fetch('/api/page-permissions/grant', {
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
const response = await fetch(`${apiUrl}/page-permissions/grant`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -230,7 +232,8 @@ class PagePermissionManager {
*/
async getUserPagePermissions(userId) {
try {
const response = await fetch(`/api/users/${userId}/page-permissions`, {
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
const response = await fetch(`${apiUrl}/users/${userId}/page-permissions`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
}

View File

@@ -3,9 +3,9 @@
* M-Project 작업보고서 시스템
*/
const CACHE_NAME = 'mproject-v1.0.0';
const STATIC_CACHE = 'mproject-static-v1.0.0';
const DYNAMIC_CACHE = 'mproject-dynamic-v1.0.0';
const CACHE_NAME = 'mproject-v1.0.1';
const STATIC_CACHE = 'mproject-static-v1.0.1';
const DYNAMIC_CACHE = 'mproject-dynamic-v1.0.1';
// 캐시할 정적 리소스
const STATIC_ASSETS = [