diff --git a/system3-nonconformance/api/routers/page_permissions.py b/system3-nonconformance/api/routers/page_permissions.py index 18731b0..6af34e1 100644 --- a/system3-nonconformance/api/routers/page_permissions.py +++ b/system3-nonconformance/api/routers/page_permissions.py @@ -50,9 +50,36 @@ DEFAULT_PAGES = { 'issues_management': {'title': '관리함', 'default_access': False}, 'issues_archive': {'title': '폐기함', 'default_access': False}, 'issues_dashboard': {'title': '현황판', 'default_access': True}, - 'reports': {'title': '보고서', 'default_access': False} + 'projects_manage': {'title': '프로젝트 관리', 'default_access': False}, + 'daily_work': {'title': '일일 공수', 'default_access': False}, + 'reports': {'title': '보고서', 'default_access': False}, + 'reports_daily': {'title': '일일보고서', 'default_access': False}, + 'users_manage': {'title': '사용자 관리', 'default_access': False} } + +def check_page_access(user_id: int, page_name: str, db: Session) -> bool: + """동기 헬퍼: 사용자의 페이지 접근 권한 확인 (라우터 내부 호출용)""" + # admin은 모든 페이지 접근 가능 + user = db.query(User).filter(User.id == user_id).first() + if user and user.role == UserRole.admin: + return True + + # 유효하지 않은 페이지 + if page_name not in DEFAULT_PAGES: + return False + + # 개별 권한 확인 + permission = db.query(UserPagePermission).filter( + UserPagePermission.user_id == user_id, + UserPagePermission.page_name == page_name + ).first() + if permission: + return permission.can_access + + # 기본 권한 + return DEFAULT_PAGES[page_name]['default_access'] + @router.post("/page-permissions/grant") async def grant_page_permission( request: PagePermissionRequest, diff --git a/system3-nonconformance/api/routers/reports.py b/system3-nonconformance/api/routers/reports.py index 292a945..7351a6e 100644 --- a/system3-nonconformance/api/routers/reports.py +++ b/system3-nonconformance/api/routers/reports.py @@ -1,5 +1,6 @@ from fastapi import APIRouter, Depends, HTTPException, Request -from fastapi.responses import StreamingResponse +from fastapi.encoders import jsonable_encoder +from fastapi.responses import JSONResponse, StreamingResponse from sqlalchemy.orm import Session from sqlalchemy import func, and_, or_ from datetime import datetime, date @@ -13,9 +14,10 @@ from openpyxl.drawing.image import Image as XLImage import os from database.database import get_db -from database.models import Issue, IssueStatus, IssueCategory, User, UserRole, ReviewStatus +from database.models import Issue, DailyWork, IssueStatus, IssueCategory, User, UserRole, ReviewStatus from database import schemas from routers.auth import get_current_user +from routers.page_permissions import check_page_access from utils.tkuser_client import get_token_from_request import utils.tkuser_client as tkuser_client @@ -30,9 +32,14 @@ async def generate_report_summary( """보고서 요약 생성""" start_date = report_request.start_date end_date = report_request.end_date - - total_hours = 0 - + + # 일일 공수 합계 + daily_works = db.query(DailyWork).filter( + DailyWork.date >= start_date.date(), + DailyWork.date <= end_date.date() + ).all() + total_hours = sum(w.total_hours for w in daily_works) + # 이슈 통계 issues_query = db.query(Issue).filter( Issue.report_date >= start_date, @@ -111,6 +118,29 @@ async def get_report_issues( "detail_notes": issue.detail_notes } for issue in issues] +@router.get("/daily-works") +async def get_report_daily_works( + start_date: datetime, + end_date: datetime, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """보고서용 일일 공수 목록""" + works = db.query(DailyWork).filter( + DailyWork.date >= start_date.date(), + DailyWork.date <= end_date.date() + ).order_by(DailyWork.date).all() + + return [{ + "date": work.date, + "worker_count": work.worker_count, + "regular_hours": work.regular_hours, + "overtime_workers": work.overtime_workers, + "overtime_hours": work.overtime_hours, + "overtime_total": work.overtime_total, + "total_hours": work.total_hours + } for work in works] + @router.get("/daily-preview") async def preview_daily_report( project_id: int, @@ -120,9 +150,9 @@ async def preview_daily_report( ): """일일보고서 미리보기 - 추출될 항목 목록""" - # 권한 확인 - if current_user.role != UserRole.admin: - raise HTTPException(status_code=403, detail="품질팀만 접근 가능합니다") + # 권한 확인: admin 또는 reports/reports_daily 페이지 권한 보유자 + if current_user.role != UserRole.admin and not check_page_access(current_user.id, 'reports', db) and not check_page_access(current_user.id, 'reports_daily', db): + raise HTTPException(status_code=403, detail="보고서 접근 권한이 없습니다") # 프로젝트 확인 (tkuser API) token = get_token_from_request(request) @@ -150,15 +180,15 @@ async def preview_daily_report( # 통계 계산 stats = calculate_project_stats(issues) - # 이슈 리스트를 schema로 변환 + # 이슈 리스트를 schema로 변환 후 명시적 직렬화 (브라우저 호환성) issues_data = [schemas.Issue.from_orm(issue) for issue in issues] - return { + return JSONResponse(content=jsonable_encoder({ "project": project, "stats": stats, "issues": issues_data, "total_issues": len(issues) - } + })) @router.post("/daily-export") async def export_daily_report( @@ -167,11 +197,11 @@ async def export_daily_report( current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): - """품질팀용 일일보고서 엑셀 내보내기""" + """일일보고서 엑셀 내보내기""" - # 권한 확인 (품질팀만 접근 가능) - if current_user.role != UserRole.admin: - raise HTTPException(status_code=403, detail="품질팀만 접근 가능합니다") + # 권한 확인: admin 또는 reports/reports_daily 페이지 권한 보유자 + if current_user.role != UserRole.admin and not check_page_access(current_user.id, 'reports', db) and not check_page_access(current_user.id, 'reports_daily', db): + raise HTTPException(status_code=403, detail="보고서 접근 권한이 없습니다") # 프로젝트 확인 (tkuser API) token = get_token_from_request(request) @@ -705,15 +735,18 @@ async def export_daily_report( today = date.today().strftime('%Y%m%d') filename = f"{project['project_name']}_일일보고서_{today}.xlsx" - # 한글 파일명을 위한 URL 인코딩 + # 한글 파일명을 위한 URL 인코딩 (ASCII 폴백 + UTF-8 인코딩 둘 다 제공) from urllib.parse import quote encoded_filename = quote(filename.encode('utf-8')) + ascii_filename = f"daily_report_{today}.xlsx" + excel_data = excel_buffer.read() return StreamingResponse( - io.BytesIO(excel_buffer.read()), + io.BytesIO(excel_data), media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers={ - "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}" + "Content-Disposition": f"attachment; filename=\"{ascii_filename}\"; filename*=UTF-8''{encoded_filename}", + "Content-Length": str(len(excel_data)), } ) diff --git a/system3-nonconformance/web/nginx.conf b/system3-nonconformance/web/nginx.conf index b0fae54..9d16821 100644 --- a/system3-nonconformance/web/nginx.conf +++ b/system3-nonconformance/web/nginx.conf @@ -41,6 +41,11 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; + + # 엑셀 생성 등 시간이 걸리는 API 대응 + proxy_read_timeout 120s; + proxy_send_timeout 120s; + proxy_buffering off; } # 업로드 파일 diff --git a/system3-nonconformance/web/reports-daily.html b/system3-nonconformance/web/reports-daily.html index 6756ade..62810ea 100644 --- a/system3-nonconformance/web/reports-daily.html +++ b/system3-nonconformance/web/reports-daily.html @@ -11,17 +11,22 @@ - - - -
+ -