fix: 일일보고서 권한 체크를 페이지 권한 기반으로 변경 및 Edge 호환성 개선

- reports.py: role==admin 하드코딩 → check_page_access로 변경하여
  reports/reports_daily 페이지 권한 보유자도 미리보기/엑셀 내보내기 가능
- page_permissions.py: 동기 헬퍼 함수 check_page_access() 추가
  (기존 async API 엔드포인트를 await 없이 호출하는 버그 해결)
- reports-daily.html: 에러 핸들링 강화 (401/403 구분), blob download
  revokeObjectURL 지연 처리 (Edge 호환)
- nginx.conf: proxy_read_timeout/proxy_buffering off 추가
- reports.py: JSONResponse+jsonable_encoder로 명시적 직렬화,
  Content-Disposition에 ASCII 폴백 파일명 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-04 13:50:35 +09:00
parent 39b2139ea1
commit 87af06ca9c
4 changed files with 142 additions and 34 deletions

View File

@@ -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,

View File

@@ -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)),
}
)