from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from typing import List, Optional from datetime import datetime from database.database import get_db from database.models import Issue, User, Project, ReviewStatus, DisposalReasonType from database.schemas import ( InboxIssue, IssueDisposalRequest, IssueReviewRequest, IssueStatusUpdateRequest, ModificationLogEntry ) from routers.auth import get_current_user, get_current_admin from routers.page_permissions import check_page_access router = APIRouter(prefix="/api/inbox", tags=["inbox"]) @router.get("/", response_model=List[InboxIssue]) async def get_inbox_issues( project_id: Optional[int] = Query(None, description="프로젝트 ID로 필터링"), skip: int = Query(0, ge=0, description="건너뛸 항목 수"), limit: int = Query(100, ge=1, le=1000, description="가져올 항목 수"), current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ 수신함 - 검토 대기 중인 부적합 목록 조회 """ query = db.query(Issue).filter(Issue.review_status == ReviewStatus.pending_review) # 프로젝트 필터링 if project_id: query = query.filter(Issue.project_id == project_id) # 최신순 정렬 query = query.order_by(Issue.report_date.desc()) issues = query.offset(skip).limit(limit).all() return issues @router.post("/{issue_id}/dispose") async def dispose_issue( issue_id: int, disposal_request: IssueDisposalRequest, current_user: User = Depends(get_current_user), # 수신함 권한이 있는 사용자 db: Session = Depends(get_db) ): """ 부적합 폐기 처리 """ # 수신함 페이지 권한 확인 if not check_page_access(current_user.id, 'issues_inbox', db): raise HTTPException(status_code=403, detail="수신함 접근 권한이 없습니다.") # 부적합 조회 issue = db.query(Issue).filter(Issue.id == issue_id).first() if not issue: raise HTTPException(status_code=404, detail="부적합을 찾을 수 없습니다.") if issue.review_status != ReviewStatus.pending_review: raise HTTPException(status_code=400, detail="검토 대기 중인 부적합만 폐기할 수 있습니다.") # 사용자 정의 사유 검증 if disposal_request.disposal_reason == DisposalReasonType.custom: if not disposal_request.custom_disposal_reason or not disposal_request.custom_disposal_reason.strip(): raise HTTPException(status_code=400, detail="사용자 정의 폐기 사유를 입력해주세요.") # 원본 데이터 보존 if not issue.original_data: issue.original_data = { "category": issue.category.value, "description": issue.description, "project_id": issue.project_id, "photo_path": issue.photo_path, "photo_path2": issue.photo_path2, "preserved_at": datetime.now().isoformat() } # 중복 처리 로직 if disposal_request.disposal_reason == DisposalReasonType.duplicate and disposal_request.duplicate_of_issue_id: # 중복 대상 이슈 확인 target_issue = db.query(Issue).filter(Issue.id == disposal_request.duplicate_of_issue_id).first() if not target_issue: raise HTTPException(status_code=404, detail="중복 대상 이슈를 찾을 수 없습니다.") # 중복 신고자를 대상 이슈에 추가 current_reporters = target_issue.duplicate_reporters or [] new_reporter = { "user_id": issue.reporter_id, "username": issue.reporter.username, "full_name": issue.reporter.full_name, "report_date": issue.report_date.isoformat() if issue.report_date else None, "added_at": datetime.now().isoformat() } # 중복 체크 (이미 추가된 신고자인지 확인) existing_reporter = next((r for r in current_reporters if r.get("user_id") == issue.reporter_id), None) if not existing_reporter: current_reporters.append(new_reporter) target_issue.duplicate_reporters = current_reporters # 현재 이슈에 중복 대상 설정 issue.duplicate_of_issue_id = disposal_request.duplicate_of_issue_id # 폐기 처리 issue.review_status = ReviewStatus.disposed issue.disposal_reason = disposal_request.disposal_reason issue.custom_disposal_reason = disposal_request.custom_disposal_reason issue.disposed_at = datetime.now() issue.reviewed_by_id = current_user.id issue.reviewed_at = datetime.now() db.commit() db.refresh(issue) return { "message": "부적합이 성공적으로 폐기되었습니다.", "issue_id": issue.id, "disposal_reason": issue.disposal_reason.value, "disposed_at": issue.disposed_at } @router.post("/{issue_id}/review") async def review_issue( issue_id: int, review_request: IssueReviewRequest, current_user: User = Depends(get_current_user), # 수신함 권한이 있는 사용자 db: Session = Depends(get_db) ): """ 부적합 검토 및 수정 """ # 수신함 페이지 권한 확인 if not check_page_access(current_user.id, 'issues_inbox', db): raise HTTPException(status_code=403, detail="수신함 접근 권한이 없습니다.") # 부적합 조회 issue = db.query(Issue).filter(Issue.id == issue_id).first() if not issue: raise HTTPException(status_code=404, detail="부적합을 찾을 수 없습니다.") if issue.review_status != ReviewStatus.pending_review: raise HTTPException(status_code=400, detail="검토 대기 중인 부적합만 수정할 수 있습니다.") # 원본 데이터 보존 (최초 1회만) if not issue.original_data: issue.original_data = { "category": issue.category.value, "description": issue.description, "project_id": issue.project_id, "photo_path": issue.photo_path, "photo_path2": issue.photo_path2, "preserved_at": datetime.now().isoformat() } # 수정 이력 준비 modifications = [] current_time = datetime.now() # 프로젝트 변경 if review_request.project_id is not None and review_request.project_id != issue.project_id: # 프로젝트 존재 확인 if review_request.project_id != 0: # 0은 프로젝트 없음을 의미 project = db.query(Project).filter(Project.id == review_request.project_id).first() if not project: raise HTTPException(status_code=400, detail="존재하지 않는 프로젝트입니다.") modifications.append({ "field": "project_id", "old_value": issue.project_id, "new_value": review_request.project_id, "modified_at": current_time.isoformat(), "modified_by": current_user.id }) issue.project_id = review_request.project_id if review_request.project_id != 0 else None # 카테고리 변경 if review_request.category is not None and review_request.category != issue.category: modifications.append({ "field": "category", "old_value": issue.category.value, "new_value": review_request.category.value, "modified_at": current_time.isoformat(), "modified_by": current_user.id }) issue.category = review_request.category # 설명 변경 if review_request.description is not None and review_request.description != issue.description: modifications.append({ "field": "description", "old_value": issue.description, "new_value": review_request.description, "modified_at": current_time.isoformat(), "modified_by": current_user.id }) issue.description = review_request.description # 수정 이력 업데이트 if modifications: if issue.modification_log is None: issue.modification_log = [] issue.modification_log.extend(modifications) # SQLAlchemy에 변경사항 알림 from sqlalchemy.orm.attributes import flag_modified flag_modified(issue, "modification_log") # 검토자 정보 업데이트 issue.reviewed_by_id = current_user.id issue.reviewed_at = current_time db.commit() db.refresh(issue) return { "message": "부적합 검토가 완료되었습니다.", "issue_id": issue.id, "modifications_count": len(modifications), "modifications": modifications, "reviewed_at": issue.reviewed_at } @router.post("/{issue_id}/status") async def update_issue_status( issue_id: int, status_request: IssueStatusUpdateRequest, current_user: User = Depends(get_current_user), # 수신함 권한이 있는 사용자 db: Session = Depends(get_db) ): """ 부적합 최종 상태 결정 (진행 중 / 완료) """ # 수신함 페이지 권한 확인 if not check_page_access(current_user.id, 'issues_inbox', db): raise HTTPException(status_code=403, detail="수신함 접근 권한이 없습니다.") # 부적합 조회 issue = db.query(Issue).filter(Issue.id == issue_id).first() if not issue: raise HTTPException(status_code=404, detail="부적합을 찾을 수 없습니다.") if issue.review_status not in [ReviewStatus.pending_review, ReviewStatus.in_progress]: raise HTTPException(status_code=400, detail="이미 처리가 완료된 부적합입니다.") # 상태 변경 검증 if status_request.review_status not in [ReviewStatus.in_progress, ReviewStatus.completed]: raise HTTPException(status_code=400, detail="진행 중 또는 완료 상태만 설정할 수 있습니다.") # 원본 데이터 보존 (최초 1회만) if not issue.original_data: issue.original_data = { "category": issue.category.value, "description": issue.description, "project_id": issue.project_id, "photo_path": issue.photo_path, "photo_path2": issue.photo_path2, "preserved_at": datetime.now().isoformat() } # 상태 변경 old_status = issue.review_status issue.review_status = status_request.review_status issue.reviewed_by_id = current_user.id issue.reviewed_at = datetime.now() # 노트가 있으면 detail_notes에 추가 if status_request.notes: current_notes = issue.detail_notes or "" timestamp = datetime.now().strftime("%Y-%m-%d %H:%M") new_note = f"[{timestamp}] {current_user.username}: {status_request.notes}" issue.detail_notes = f"{current_notes}\n{new_note}".strip() db.commit() db.refresh(issue) return { "message": f"부적합 상태가 '{status_request.review_status.value}'로 변경되었습니다.", "issue_id": issue.id, "old_status": old_status.value, "new_status": issue.review_status.value, "reviewed_at": issue.reviewed_at, "destination": "관리함" if status_request.review_status in [ReviewStatus.in_progress, ReviewStatus.completed] else "수신함" } @router.get("/{issue_id}/history") async def get_issue_history( issue_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ 부적합 수정 이력 조회 """ issue = db.query(Issue).filter(Issue.id == issue_id).first() if not issue: raise HTTPException(status_code=404, detail="부적합을 찾을 수 없습니다.") return { "issue_id": issue.id, "original_data": issue.original_data, "modification_log": issue.modification_log or [], "review_status": issue.review_status.value, "reviewed_by_id": issue.reviewed_by_id, "reviewed_at": issue.reviewed_at, "disposal_info": { "disposal_reason": issue.disposal_reason.value if issue.disposal_reason else None, "custom_disposal_reason": issue.custom_disposal_reason, "disposed_at": issue.disposed_at } if issue.review_status == ReviewStatus.disposed else None } @router.get("/statistics") async def get_inbox_statistics( current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ 수신함 통계 정보 """ # 검토 대기 중 pending_count = db.query(Issue).filter(Issue.review_status == ReviewStatus.pending_review).count() # 오늘 처리된 건수 today = datetime.now().date() today_processed = db.query(Issue).filter( Issue.reviewed_at >= today, Issue.review_status != ReviewStatus.pending_review ).count() # 폐기된 건수 disposed_count = db.query(Issue).filter(Issue.review_status == ReviewStatus.disposed).count() # 관리함으로 이동한 건수 management_count = db.query(Issue).filter( Issue.review_status.in_([ReviewStatus.in_progress, ReviewStatus.completed]) ).count() return { "pending_review": pending_count, "today_processed": today_processed, "total_disposed": disposed_count, "total_in_management": management_count, "total_issues": db.query(Issue).count() } @router.get("/management-issues") async def get_management_issues( project_id: Optional[int] = None, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """관리함 이슈 목록 조회 (중복 선택용)""" try: query = db.query(Issue).filter( Issue.review_status.in_([ReviewStatus.in_progress, ReviewStatus.completed]) ) # 프로젝트 필터 적용 if project_id: query = query.filter(Issue.project_id == project_id) issues = query.order_by(Issue.reviewed_at.desc()).limit(50).all() # 간단한 형태로 반환 (제목과 ID만) result = [] for issue in issues: result.append({ "id": issue.id, "description": issue.description[:100] + "..." if len(issue.description) > 100 else issue.description, "category": issue.category.value, "reporter_name": issue.reporter.full_name or issue.reporter.username, "reviewed_at": issue.reviewed_at.isoformat() if issue.reviewed_at else None, "duplicate_count": len(issue.duplicate_reporters) if issue.duplicate_reporters else 0 }) return result except Exception as e: raise HTTPException(status_code=500, detail=f"관리함 이슈 조회 중 오류가 발생했습니다: {str(e)}")