from fastapi import APIRouter, Depends, HTTPException, status 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, IssueStatus, User, UserRole, ReviewStatus from database import schemas from routers.auth import get_current_user, get_current_admin from routers.page_permissions import check_page_access from services.file_service import save_base64_image, delete_file router = APIRouter(prefix="/api/issues", tags=["issues"]) @router.post("/", response_model=schemas.Issue) async def create_issue( issue: schemas.IssueCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): # 이미지 저장 (최대 5장) photo_paths = {} for i in range(1, 6): photo_field = f"photo{i if i > 1 else ''}" path_field = f"photo_path{i if i > 1 else ''}" photo_data = getattr(issue, photo_field, None) if photo_data: photo_paths[path_field] = save_base64_image(photo_data) else: photo_paths[path_field] = None # Issue 생성 db_issue = Issue( category=issue.category, description=issue.description, location_info=issue.location_info, photo_path=photo_paths.get('photo_path'), photo_path2=photo_paths.get('photo_path2'), photo_path3=photo_paths.get('photo_path3'), photo_path4=photo_paths.get('photo_path4'), photo_path5=photo_paths.get('photo_path5'), reporter_id=current_user.id, project_id=issue.project_id, status=IssueStatus.new ) db.add(db_issue) db.commit() db.refresh(db_issue) return db_issue @router.get("/", response_model=List[schemas.Issue]) async def read_issues( skip: int = 0, limit: int = 100, status: Optional[IssueStatus] = None, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): query = db.query(Issue) # 권한별 조회 제한 if current_user.role == UserRole.admin: # 관리자는 모든 이슈 조회 가능 pass else: # 일반 사용자는 본인이 등록한 이슈만 조회 가능 query = query.filter(Issue.reporter_id == current_user.id) if status: query = query.filter(Issue.status == status) # 최신순 정렬 (report_date 기준) issues = query.order_by(Issue.report_date.desc()).offset(skip).limit(limit).all() return issues @router.get("/admin/all", response_model=List[schemas.Issue]) async def read_all_issues_admin( skip: int = 0, limit: int = 100, status: Optional[IssueStatus] = None, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """이슈 관리 권한이 있는 사용자: 모든 부적합 조회""" # 이슈 관리 페이지 권한 확인 (관리함, 폐기함 등에서 사용) from routers.page_permissions import check_page_access # 관리자이거나 이슈 관리 권한이 있는 사용자만 접근 가능 if (current_user.role != 'admin' and not check_page_access(current_user.id, 'issues_manage', db) and not check_page_access(current_user.id, 'issues_inbox', db) and not check_page_access(current_user.id, 'issues_management', db) and not check_page_access(current_user.id, 'issues_archive', db)): raise HTTPException(status_code=403, detail="이슈 관리 권한이 없습니다.") query = db.query(Issue) if status: query = query.filter(Issue.status == status) # 최신순 정렬 (report_date 기준) issues = query.order_by(Issue.report_date.desc()).offset(skip).limit(limit).all() return issues @router.get("/{issue_id}", response_model=schemas.Issue) async def read_issue( 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="Issue not found") # 권한별 조회 제한 if current_user.role != UserRole.admin and issue.reporter_id != current_user.id: raise HTTPException( status_code=403, detail="본인이 등록한 부적합만 조회할 수 있습니다." ) return issue @router.put("/{issue_id}", response_model=schemas.Issue) async def update_issue( issue_id: int, issue_update: schemas.IssueUpdate, 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="Issue not found") # 권한 확인 if current_user.role == UserRole.user and issue.reporter_id != current_user.id: raise HTTPException(status_code=403, detail="Not authorized to update this issue") # 업데이트 update_data = issue_update.dict(exclude_unset=True) # 사진 업데이트 처리 (최대 5장) for i in range(1, 6): photo_field = f"photo{i if i > 1 else ''}" path_field = f"photo_path{i if i > 1 else ''}" if photo_field in update_data: # 기존 사진 삭제 existing_path = getattr(issue, path_field, None) if existing_path: delete_file(existing_path) # 새 사진 저장 if update_data[photo_field]: new_path = save_base64_image(update_data[photo_field]) update_data[path_field] = new_path else: update_data[path_field] = None # photo 필드는 제거 (DB에는 photo_path만 저장) del update_data[photo_field] # work_hours가 입력되면 자동으로 상태를 complete로 변경 if "work_hours" in update_data and update_data["work_hours"] > 0: if issue.status == IssueStatus.new: update_data["status"] = IssueStatus.complete for field, value in update_data.items(): setattr(issue, field, value) db.commit() db.refresh(issue) return issue @router.delete("/{issue_id}") async def delete_issue( issue_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): from database.models import DeletionLog import json issue = db.query(Issue).filter(Issue.id == issue_id).first() if not issue: raise HTTPException(status_code=404, detail="Issue not found") # 권한 확인 (관리자 또는 본인이 등록한 경우 삭제 가능) if current_user.role != UserRole.admin and issue.reporter_id != current_user.id: raise HTTPException(status_code=403, detail="본인이 등록한 부적합만 삭제할 수 있습니다.") # 이 이슈를 중복 대상으로 참조하는 다른 이슈들의 참조 제거 referencing_issues = db.query(Issue).filter(Issue.duplicate_of_issue_id == issue_id).all() if referencing_issues: for ref_issue in referencing_issues: ref_issue.duplicate_of_issue_id = None db.flush() # 참조 제거를 먼저 커밋 # 삭제 로그 생성 (삭제 전 데이터 저장) issue_data = { "id": issue.id, "category": issue.category.value if issue.category else None, "description": issue.description, "status": issue.status.value if issue.status else None, "reporter_id": issue.reporter_id, "project_id": issue.project_id, "report_date": issue.report_date.isoformat() if issue.report_date else None, "work_hours": issue.work_hours, "detail_notes": issue.detail_notes, "photo_path": issue.photo_path, "photo_path2": issue.photo_path2, "review_status": issue.review_status.value if issue.review_status else None, "solution": issue.solution, "responsible_department": issue.responsible_department.value if issue.responsible_department else None, "responsible_person": issue.responsible_person, "expected_completion_date": issue.expected_completion_date.isoformat() if issue.expected_completion_date else None, "actual_completion_date": issue.actual_completion_date.isoformat() if issue.actual_completion_date else None, } deletion_log = DeletionLog( entity_type="issue", entity_id=issue.id, entity_data=issue_data, deleted_by_id=current_user.id, reason=f"사용자 {current_user.username}에 의해 삭제됨" ) db.add(deletion_log) # 이미지 파일 삭제 (신고 사진 최대 5장) for i in range(1, 6): path_field = f"photo_path{i if i > 1 else ''}" path = getattr(issue, path_field, None) if path: delete_file(path) # 완료 사진 삭제 (최대 5장) for i in range(1, 6): path_field = f"completion_photo_path{i if i > 1 else ''}" path = getattr(issue, path_field, None) if path: delete_file(path) db.delete(issue) db.commit() return {"detail": "Issue deleted successfully", "logged": True} @router.post("/{issue_id}/opinion") async def add_opinion( issue_id: int, opinion_request: schemas.OpinionRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ 의견 제시 — 로그인한 모든 사용자가 사용 가능 (권한 체크 없음) solution 필드에 의견을 추가합니다. """ issue = db.query(Issue).filter(Issue.id == issue_id).first() if not issue: raise HTTPException(status_code=404, detail="부적합을 찾을 수 없습니다.") # 새 의견 형식: [작성자] (날짜시간)\n내용 now = datetime.now() date_str = now.strftime("%Y. %m. %d. %H:%M") author = current_user.full_name or current_user.username new_opinion = f"[{author}] ({date_str})\n{opinion_request.opinion}" # 기존 solution에 추가 (최신이 위로) separator = "─" * 50 if issue.solution: issue.solution = f"{new_opinion}\n{separator}\n{issue.solution}" else: issue.solution = new_opinion db.commit() db.refresh(issue) return { "message": "의견이 추가되었습니다.", "issue_id": issue.id } @router.get("/stats/summary") async def get_issue_stats( current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """이슈 통계 조회""" query = db.query(Issue) # 일반 사용자는 자신의 이슈만 if current_user.role == UserRole.user: query = query.filter(Issue.reporter_id == current_user.id) total = query.count() new = query.filter(Issue.status == IssueStatus.new).count() progress = query.filter(Issue.status == IssueStatus.progress).count() complete = query.filter(Issue.status == IssueStatus.complete).count() return { "total": total, "new": new, "progress": progress, "complete": complete } @router.put("/{issue_id}/management") async def update_issue_management( issue_id: int, management_update: schemas.ManagementUpdateRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ 관리함에서 이슈의 관리 관련 필드들을 업데이트합니다. """ # 관리함 페이지 권한 확인 if not (current_user.role == UserRole.admin or check_page_access(current_user.id, 'issues_management', 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="부적합을 찾을 수 없습니다.") # 관리함에서만 수정 가능한 필드들만 업데이트 update_data = management_update.dict(exclude_unset=True) # 완료 사진 처리 (최대 5장) completion_photo_fields = [] for i in range(1, 6): photo_field = f"completion_photo{i if i > 1 else ''}" path_field = f"completion_photo_path{i if i > 1 else ''}" if photo_field in update_data and update_data[photo_field]: completion_photo_fields.append(photo_field) try: # 기존 사진 삭제 existing_path = getattr(issue, path_field, None) if existing_path: delete_file(existing_path) # 새 사진 저장 new_path = save_base64_image(update_data[photo_field], "completion") setattr(issue, path_field, new_path) except Exception as e: raise HTTPException(status_code=400, detail=f"완료 사진 저장 실패: {str(e)}") # 나머지 필드 처리 (완료 사진 제외) for field, value in update_data.items(): if field not in completion_photo_fields: try: setattr(issue, field, value) except Exception as e: raise HTTPException(status_code=400, detail=f"필드 {field} 설정 실패: {str(e)}") try: db.commit() db.refresh(issue) except Exception as e: db.rollback() raise HTTPException(status_code=500, detail=f"데이터베이스 저장 실패: {str(e)}") return { "message": "관리 정보가 업데이트되었습니다.", "issue_id": issue.id, "updated_fields": list(update_data.keys()) } @router.post("/{issue_id}/completion-request") async def request_completion( issue_id: int, request: schemas.CompletionRequestRequest, 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="부적합을 찾을 수 없습니다.") # 진행 중 상태인지 확인 if issue.review_status != ReviewStatus.in_progress: raise HTTPException(status_code=400, detail="진행 중 상태의 부적합만 완료 신청할 수 있습니다.") # 이미 완료 신청된 경우 확인 if issue.completion_requested_at: raise HTTPException(status_code=400, detail="이미 완료 신청된 부적합입니다.") try: # 완료 사진 저장 (최대 5장) saved_paths = [] for i in range(1, 6): photo_field = f"completion_photo{i if i > 1 else ''}" path_field = f"completion_photo_path{i if i > 1 else ''}" photo_data = getattr(request, photo_field, None) if photo_data: saved_path = save_base64_image(photo_data, "completion") if saved_path: setattr(issue, path_field, saved_path) saved_paths.append(saved_path) else: raise Exception(f"{photo_field} 저장에 실패했습니다.") # 완료 신청 정보 업데이트 issue.completion_requested_at = datetime.now() issue.completion_requested_by_id = current_user.id issue.completion_comment = request.completion_comment db.commit() db.refresh(issue) return { "message": "완료 신청이 성공적으로 제출되었습니다.", "issue_id": issue.id, "completion_requested_at": issue.completion_requested_at, "completion_photo_paths": saved_paths } except Exception as e: print(f"ERROR: 완료 신청 처리 오류 - {str(e)}") db.rollback() # 업로드된 파일이 있다면 삭제 if 'saved_paths' in locals(): for path in saved_paths: try: delete_file(path) except: pass raise HTTPException(status_code=500, detail=f"완료 신청 처리 중 오류가 발생했습니다: {str(e)}") @router.post("/{issue_id}/reject-completion") async def reject_completion_request( issue_id: int, request: schemas.CompletionRejectionRequest, 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="부적합을 찾을 수 없습니다.") # 완료 신청이 있는지 확인 if not issue.completion_requested_at: raise HTTPException(status_code=400, detail="완료 신청이 없는 부적합입니다.") # 권한 확인 (관리자 또는 관리함 접근 권한이 있는 사용자) if current_user.role != UserRole.admin and not check_page_access(current_user.id, 'issues_management', db): raise HTTPException(status_code=403, detail="완료 반려 권한이 없습니다.") try: # 완료 사진 파일 삭제 (최대 5장) for i in range(1, 6): path_field = f"completion_photo_path{i if i > 1 else ''}" photo_path = getattr(issue, path_field, None) if photo_path: try: delete_file(photo_path) except Exception: pass # 완료 신청 정보 초기화 issue.completion_requested_at = None issue.completion_requested_by_id = None for i in range(1, 6): path_field = f"completion_photo_path{i if i > 1 else ''}" setattr(issue, path_field, None) issue.completion_comment = None # 완료 반려 정보 기록 (전용 필드 사용) issue.completion_rejected_at = datetime.now() issue.completion_rejected_by_id = current_user.id issue.completion_rejection_reason = request.rejection_reason # 상태는 in_progress로 유지 issue.review_status = ReviewStatus.in_progress db.commit() db.refresh(issue) return { "message": "완료 신청이 반려되었습니다.", "issue_id": issue.id, "rejection_reason": request.rejection_reason } except Exception as e: print(f"ERROR: 완료 반려 처리 오류 - {str(e)}") db.rollback() raise HTTPException(status_code=500, detail=f"완료 반려 처리 중 오류가 발생했습니다: {str(e)}")