- backend: completion_rejection_reason 등 전용 필드 추가 - 기존 management_comment에 섞여있던 완료 반려 내용 분리 - 현황판: 완료 반려 내역 별도 카드로 표시 - 관리함: 해결방안에 완료 반려 내용 제외하여 표시 - DB 마이그레이션: completion_rejected_at, completion_rejected_by_id, completion_rejection_reason 필드 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
464 lines
18 KiB
Python
464 lines
18 KiB
Python
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)
|
|
):
|
|
print(f"DEBUG: 받은 issue 데이터: {issue}")
|
|
print(f"DEBUG: project_id: {issue.project_id}")
|
|
# 이미지 저장
|
|
photo_path = None
|
|
photo_path2 = None
|
|
|
|
if issue.photo:
|
|
photo_path = save_base64_image(issue.photo)
|
|
|
|
if issue.photo2:
|
|
photo_path2 = save_base64_image(issue.photo2)
|
|
|
|
# Issue 생성
|
|
db_issue = Issue(
|
|
category=issue.category,
|
|
description=issue.description,
|
|
photo_path=photo_path,
|
|
photo_path2=photo_path2,
|
|
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)
|
|
|
|
# 첫 번째 사진이 업데이트되는 경우 처리
|
|
if "photo" in update_data:
|
|
# 기존 사진 삭제
|
|
if issue.photo_path:
|
|
delete_file(issue.photo_path)
|
|
|
|
# 새 사진 저장
|
|
if update_data["photo"]:
|
|
photo_path = save_base64_image(update_data["photo"])
|
|
update_data["photo_path"] = photo_path
|
|
else:
|
|
update_data["photo_path"] = None
|
|
|
|
# photo 필드는 제거 (DB에는 photo_path만 저장)
|
|
del update_data["photo"]
|
|
|
|
# 두 번째 사진이 업데이트되는 경우 처리
|
|
if "photo2" in update_data:
|
|
# 기존 사진 삭제
|
|
if issue.photo_path2:
|
|
delete_file(issue.photo_path2)
|
|
|
|
# 새 사진 저장
|
|
if update_data["photo2"]:
|
|
photo_path2 = save_base64_image(update_data["photo2"])
|
|
update_data["photo_path2"] = photo_path2
|
|
else:
|
|
update_data["photo_path2"] = None
|
|
|
|
# photo2 필드는 제거 (DB에는 photo_path2만 저장)
|
|
del update_data["photo2"]
|
|
|
|
# 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:
|
|
print(f"DEBUG: {len(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)
|
|
|
|
# 이미지 파일 삭제
|
|
if issue.photo_path:
|
|
delete_file(issue.photo_path)
|
|
if issue.photo_path2:
|
|
delete_file(issue.photo_path2)
|
|
if issue.completion_photo_path:
|
|
delete_file(issue.completion_photo_path)
|
|
|
|
db.delete(issue)
|
|
db.commit()
|
|
return {"detail": "Issue deleted successfully", "logged": True}
|
|
|
|
@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)
|
|
):
|
|
"""
|
|
관리함에서 이슈의 관리 관련 필드들을 업데이트합니다.
|
|
"""
|
|
print(f"DEBUG: Received management update for issue {issue_id}")
|
|
print(f"DEBUG: Update data: {management_update}")
|
|
print(f"DEBUG: Current user: {current_user.username}")
|
|
|
|
# 관리함 페이지 권한 확인
|
|
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="부적합을 찾을 수 없습니다.")
|
|
|
|
print(f"DEBUG: Found issue: {issue.id}")
|
|
|
|
# 관리함에서만 수정 가능한 필드들만 업데이트
|
|
update_data = management_update.dict(exclude_unset=True)
|
|
print(f"DEBUG: Update data dict: {update_data}")
|
|
|
|
for field, value in update_data.items():
|
|
print(f"DEBUG: Processing field {field} = {value}")
|
|
if field == 'completion_photo' and value:
|
|
# 완료 사진 업로드 처리
|
|
try:
|
|
completion_photo_path = save_base64_image(value, "completion")
|
|
setattr(issue, 'completion_photo_path', completion_photo_path)
|
|
print(f"DEBUG: Saved completion photo: {completion_photo_path}")
|
|
except Exception as e:
|
|
print(f"DEBUG: Photo save error: {str(e)}")
|
|
raise HTTPException(status_code=400, detail=f"완료 사진 저장 실패: {str(e)}")
|
|
elif field != 'completion_photo': # completion_photo는 위에서 처리됨
|
|
try:
|
|
setattr(issue, field, value)
|
|
print(f"DEBUG: Set {field} = {value}")
|
|
except Exception as e:
|
|
print(f"DEBUG: Field set error for {field}: {str(e)}")
|
|
raise HTTPException(status_code=400, detail=f"필드 {field} 설정 실패: {str(e)}")
|
|
|
|
try:
|
|
db.commit()
|
|
db.refresh(issue)
|
|
print(f"DEBUG: Successfully updated issue {issue.id}")
|
|
except Exception as e:
|
|
print(f"DEBUG: Database commit error: {str(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:
|
|
print(f"DEBUG: 완료 신청 시작 - Issue ID: {issue_id}, User: {current_user.username}")
|
|
|
|
# 완료 사진 저장
|
|
completion_photo_path = None
|
|
if request.completion_photo:
|
|
print(f"DEBUG: 완료 사진 저장 시작")
|
|
completion_photo_path = save_base64_image(request.completion_photo, "completion")
|
|
print(f"DEBUG: 완료 사진 저장 완료 - Path: {completion_photo_path}")
|
|
|
|
if not completion_photo_path:
|
|
raise Exception("완료 사진 저장에 실패했습니다.")
|
|
|
|
# 완료 신청 정보 업데이트
|
|
print(f"DEBUG: DB 업데이트 시작")
|
|
issue.completion_requested_at = datetime.now()
|
|
issue.completion_requested_by_id = current_user.id
|
|
issue.completion_photo_path = completion_photo_path
|
|
issue.completion_comment = request.completion_comment
|
|
|
|
db.commit()
|
|
db.refresh(issue)
|
|
print(f"DEBUG: DB 업데이트 완료")
|
|
|
|
return {
|
|
"message": "완료 신청이 성공적으로 제출되었습니다.",
|
|
"issue_id": issue.id,
|
|
"completion_requested_at": issue.completion_requested_at,
|
|
"completion_photo_path": completion_photo_path
|
|
}
|
|
|
|
except Exception as e:
|
|
print(f"ERROR: 완료 신청 처리 오류 - {str(e)}")
|
|
db.rollback()
|
|
# 업로드된 파일이 있다면 삭제
|
|
if 'completion_photo_path' in locals() and completion_photo_path:
|
|
try:
|
|
delete_file(completion_photo_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:
|
|
print(f"DEBUG: 완료 반려 시작 - Issue ID: {issue_id}, User: {current_user.username}")
|
|
|
|
# 완료 사진 파일 삭제
|
|
if issue.completion_photo_path:
|
|
try:
|
|
delete_file(issue.completion_photo_path)
|
|
print(f"DEBUG: 완료 사진 삭제 완료")
|
|
except Exception as e:
|
|
print(f"WARNING: 완료 사진 삭제 실패 - {str(e)}")
|
|
|
|
# 완료 신청 정보 초기화
|
|
issue.completion_requested_at = None
|
|
issue.completion_requested_by_id = None
|
|
issue.completion_photo_path = 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)
|
|
print(f"DEBUG: 완료 반려 처리 완료")
|
|
|
|
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)}")
|