🐛 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:
240
DEPLOYMENT_GUIDE_20251028.md
Normal file
240
DEPLOYMENT_GUIDE_20251028.md
Normal 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. 데이터베이스 롤백 (필요시)
|
||||||
|
# 백업 파일에서 복원
|
||||||
|
```
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -324,3 +324,13 @@ class ProjectDailyWork(ProjectDailyWorkBase):
|
|||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
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
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ pydantic-settings==2.1.0
|
|||||||
pillow==10.1.0
|
pillow==10.1.0
|
||||||
pillow-heif==0.13.0
|
pillow-heif==0.13.0
|
||||||
reportlab==4.0.7
|
reportlab==4.0.7
|
||||||
|
openpyxl==3.1.2
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -6,7 +6,7 @@ from typing import List
|
|||||||
from database.database import get_db
|
from database.database import get_db
|
||||||
from database.models import Issue, User, ReviewStatus
|
from database.models import Issue, User, ReviewStatus
|
||||||
from database.schemas import (
|
from database.schemas import (
|
||||||
ManagementUpdateRequest, AdditionalInfoUpdateRequest, Issue as IssueSchema
|
ManagementUpdateRequest, AdditionalInfoUpdateRequest, Issue as IssueSchema, DailyReportStats
|
||||||
)
|
)
|
||||||
from routers.auth import get_current_user
|
from routers.auth import get_current_user
|
||||||
from routers.page_permissions import check_page_access
|
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_at": issue.additional_info_updated_at,
|
||||||
"additional_info_updated_by_id": issue.additional_info_updated_by_id
|
"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
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func, and_, or_
|
||||||
from datetime import datetime
|
from datetime import datetime, date
|
||||||
from typing import List
|
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.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 database import schemas
|
||||||
from routers.auth import get_current_user
|
from routers.auth import get_current_user
|
||||||
|
|
||||||
@@ -128,3 +133,222 @@ async def get_report_daily_works(
|
|||||||
"overtime_total": work.overtime_total,
|
"overtime_total": work.overtime_total,
|
||||||
"total_hours": work.total_hours
|
"total_hours": work.total_hours
|
||||||
} for work in works]
|
} 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)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -339,7 +339,8 @@
|
|||||||
async function loadProjects() {
|
async function loadProjects() {
|
||||||
try {
|
try {
|
||||||
// API에서 최신 프로젝트 데이터 가져오기
|
// API에서 최신 프로젝트 데이터 가져오기
|
||||||
const response = await fetch('/api/projects/', {
|
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
|
||||||
|
const response = await fetch(`${apiUrl}/projects/`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
|||||||
@@ -287,7 +287,8 @@
|
|||||||
// 프로젝트 로드
|
// 프로젝트 로드
|
||||||
async function loadProjects() {
|
async function loadProjects() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/projects/', {
|
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
|
||||||
|
const response = await fetch(`${apiUrl}/projects/`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
|||||||
@@ -323,7 +323,8 @@
|
|||||||
// 데이터 로드 함수들
|
// 데이터 로드 함수들
|
||||||
async function loadProjects() {
|
async function loadProjects() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/projects/', {
|
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
|
||||||
|
const response = await fetch(`${apiUrl}/projects/`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
|||||||
@@ -668,7 +668,8 @@
|
|||||||
async function loadProjects() {
|
async function loadProjects() {
|
||||||
console.log('🔄 프로젝트 로드 시작');
|
console.log('🔄 프로젝트 로드 시작');
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/projects/', {
|
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
|
||||||
|
const response = await fetch(`${apiUrl}/projects/`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
|||||||
@@ -465,7 +465,8 @@
|
|||||||
// 프로젝트 로드
|
// 프로젝트 로드
|
||||||
async function loadProjects() {
|
async function loadProjects() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/projects/', {
|
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
|
||||||
|
const response = await fetch(`${apiUrl}/projects/`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -2548,5 +2549,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
383
frontend/reports-daily.html
Normal file
383
frontend/reports-daily.html
Normal 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>
|
||||||
111
frontend/reports-monthly.html
Normal file
111
frontend/reports-monthly.html
Normal 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>
|
||||||
110
frontend/reports-weekly.html
Normal file
110
frontend/reports-weekly.html
Normal 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
212
frontend/reports.html
Normal 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>
|
||||||
@@ -93,7 +93,33 @@ class CommonHeader {
|
|||||||
url: '/reports.html',
|
url: '/reports.html',
|
||||||
pageName: 'reports',
|
pageName: 'reports',
|
||||||
color: 'text-red-600',
|
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',
|
id: 'projects_manage',
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ class PagePermissionManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// API에서 사용자별 페이지 권한 가져오기
|
// 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: {
|
headers: {
|
||||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||||
}
|
}
|
||||||
@@ -198,7 +199,8 @@ class PagePermissionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -230,7 +232,8 @@ class PagePermissionManager {
|
|||||||
*/
|
*/
|
||||||
async getUserPagePermissions(userId) {
|
async getUserPagePermissions(userId) {
|
||||||
try {
|
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: {
|
headers: {
|
||||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
* M-Project 작업보고서 시스템
|
* M-Project 작업보고서 시스템
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CACHE_NAME = 'mproject-v1.0.0';
|
const CACHE_NAME = 'mproject-v1.0.1';
|
||||||
const STATIC_CACHE = 'mproject-static-v1.0.0';
|
const STATIC_CACHE = 'mproject-static-v1.0.1';
|
||||||
const DYNAMIC_CACHE = 'mproject-dynamic-v1.0.0';
|
const DYNAMIC_CACHE = 'mproject-dynamic-v1.0.1';
|
||||||
|
|
||||||
// 캐시할 정적 리소스
|
// 캐시할 정적 리소스
|
||||||
const STATIC_ASSETS = [
|
const STATIC_ASSETS = [
|
||||||
|
|||||||
Reference in New Issue
Block a user