From ef5c5e63cb5b7936f4a5379bced27e619233e03e Mon Sep 17 00:00:00 2001 From: hyungi Date: Sat, 6 Dec 2025 13:51:28 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=9D=BC=EC=9D=BC=EB=B3=B4=EA=B3=A0?= =?UTF-8?q?=EC=84=9C=20CORS=20=EC=97=90=EB=9F=AC=20=EB=B0=8F=20datetime=20?= =?UTF-8?q?=ED=98=B8=ED=99=98=EC=84=B1=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 전역 예외 처리기에 CORS 헤더 추가 (500 에러에서도 CORS 헤더 포함) - datetime.date 객체의 timestamp/tzinfo 호환성 문제 해결 - 일일보고서 접근 권한을 모든 로그인 사용자로 확대 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- backend/main.py | 8 +++++++- backend/routers/reports.py | 39 ++++++++++++++++++++++++-------------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/backend/main.py b/backend/main.py index 2aea158..fe693bb 100644 --- a/backend/main.py +++ b/backend/main.py @@ -60,9 +60,15 @@ async def health_check(): # 전역 예외 처리 @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): + # CORS 헤더 추가 (500 에러에서도 CORS 헤더가 필요) return JSONResponse( status_code=500, - content={"detail": f"Internal server error: {str(exc)}"} + content={"detail": f"Internal server error: {str(exc)}"}, + headers={ + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "*" + } ) if __name__ == "__main__": diff --git a/backend/routers/reports.py b/backend/routers/reports.py index 30a95e6..6d5671d 100644 --- a/backend/routers/reports.py +++ b/backend/routers/reports.py @@ -145,10 +145,6 @@ async def preview_daily_report( ): """일일보고서 미리보기 - 추출될 항목 목록""" - # 권한 확인 - if current_user.role != UserRole.admin: - raise HTTPException(status_code=403, detail="품질팀만 접근 가능합니다") - # 프로젝트 확인 project = db.query(Project).filter(Project.id == project_id).first() if not project: @@ -169,7 +165,7 @@ async def preview_daily_report( issues = issues_query.all() # 정렬: 지연 -> 진행중 -> 완료됨 순으로, 같은 상태 내에서는 신고일 최신순 - issues = sorted(issues, key=lambda x: (get_issue_priority(x), -x.report_date.timestamp() if x.report_date else 0)) + issues = sorted(issues, key=lambda x: (get_issue_priority(x), -get_timestamp(x.report_date))) # 통계 계산 stats = calculate_project_stats(issues) @@ -190,11 +186,7 @@ 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="품질팀만 접근 가능합니다") + """일일보고서 엑셀 내보내기""" # 프로젝트 확인 project = db.query(Project).filter(Project.id == request.project_id).first() @@ -223,8 +215,18 @@ async def export_daily_report( not_exported_after_completion.append(issue) elif issue.actual_completion_date: # actual_completion_date가 있는 경우: 완료일과 마지막 추출일 비교 - completion_date = issue.actual_completion_date.replace(tzinfo=None) if issue.actual_completion_date.tzinfo else issue.actual_completion_date - export_date = issue.last_exported_at.replace(tzinfo=None) if issue.last_exported_at.tzinfo else issue.last_exported_at + # actual_completion_date는 date 또는 datetime일 수 있음 + if isinstance(issue.actual_completion_date, datetime): + completion_date = issue.actual_completion_date.replace(tzinfo=None) if issue.actual_completion_date.tzinfo else issue.actual_completion_date + else: + # date 타입인 경우 datetime으로 변환 + completion_date = datetime.combine(issue.actual_completion_date, datetime.min.time()) + + if isinstance(issue.last_exported_at, datetime): + export_date = issue.last_exported_at.replace(tzinfo=None) if issue.last_exported_at.tzinfo else issue.last_exported_at + else: + export_date = datetime.combine(issue.last_exported_at, datetime.min.time()) + if completion_date > export_date: # 완료일이 마지막 추출일보다 나중 -> 완료 후 아직 추출 안됨 -> 진행 중 시트에 표시 not_exported_after_completion.append(issue) @@ -236,14 +238,14 @@ async def export_daily_report( in_progress_issues = in_progress_only + not_exported_after_completion # 진행 중 시트 정렬: 지연중 -> 진행중 -> 완료됨 순서 - in_progress_issues = sorted(in_progress_issues, key=lambda x: (get_issue_priority(x), -x.report_date.timestamp() if x.report_date else 0)) + in_progress_issues = sorted(in_progress_issues, key=lambda x: (get_issue_priority(x), -get_timestamp(x.report_date))) # "완료됨" 시트용: 완료 항목 중 "완료 후 추출된 것"만 (진행 중 시트에 표시되는 것 제외) not_exported_ids = {issue.id for issue in not_exported_after_completion} completed_issues = [issue for issue in all_completed if issue.id not in not_exported_ids] # 완료됨 시트도 정렬 (완료일 최신순) - completed_issues = sorted(completed_issues, key=lambda x: -x.actual_completion_date.timestamp() if x.actual_completion_date else 0) + completed_issues = sorted(completed_issues, key=lambda x: -get_timestamp(x.actual_completion_date)) # 웹과 동일한 로직: 진행중 + 완료를 함께 정렬하여 순번 할당 # (웹에서는 in_progress와 completed를 함께 가져와서 전체를 reviewed_at 순으로 정렬 후 순번 매김) @@ -871,3 +873,12 @@ def get_issue_status_header_color(issue: Issue) -> str: elif priority == 3: # 완료 return "92D050" # 진한 초록색 return "4472C4" # 기본 파란색 + +def get_timestamp(dt) -> float: + """date 또는 datetime 객체에서 timestamp 반환""" + if dt is None: + return 0 + if isinstance(dt, datetime): + return dt.timestamp() + # date 타입인 경우 datetime으로 변환 + return datetime.combine(dt, datetime.min.time()).timestamp()