Files
M-Project/backend/routers/issues.py
Hyungi Ahn 6e240f2296 feat: 완료 신청 API 엔드포인트 구현
🔧 백엔드 API 추가:
- POST /api/issues/{issue_id}/completion-request
- 완료 사진 Base64 업로드 및 저장
- 완료 코멘트 저장
- 완료 신청 시간 및 신청자 기록

📊 데이터 모델 확장:
- completion_requested_at: 완료 신청 시간
- completion_requested_by_id: 완료 신청자 ID
- completion_photo_path: 완료 사진 경로
- completion_comment: 완료 코멘트

🛡️ 검증 로직:
- 진행 중 상태만 완료 신청 가능
- 중복 완료 신청 방지
- 파일 업로드 오류 시 롤백 처리

🔄 프론트엔드 연동:
- refreshDashboard() 함수 추가
- 완료 신청 후 현황판 자동 새로고침
- 오류 처리 및 사용자 피드백

Expected Result:
 완료 신청 API 정상 작동
 완료 사진 업로드 및 저장
 완료 대기 상태로 변경
 404 오류 해결
2025-10-26 12:55:45 +09:00

346 lines
12 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)
):
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:
raise HTTPException(status_code=403, detail="Only admin can delete issues")
# 이미지 파일 삭제
if issue.photo_path:
delete_file(issue.photo_path)
db.delete(issue)
db.commit()
return {"detail": "Issue deleted successfully"}
@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:
# 완료 사진 저장
completion_photo_path = None
if request.completion_photo:
completion_photo_path = save_base64_image(request.completion_photo, "completion")
# 완료 신청 정보 업데이트
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)
return {
"message": "완료 신청이 성공적으로 제출되었습니다.",
"issue_id": issue.id,
"completion_requested_at": issue.completion_requested_at,
"completion_photo_path": completion_photo_path
}
except Exception as e:
db.rollback()
# 업로드된 파일이 있다면 삭제
if completion_photo_path:
try:
delete_file(completion_photo_path)
except:
pass
raise HTTPException(status_code=500, detail=f"완료 신청 처리 중 오류가 발생했습니다: {str(e)}")