diff --git a/backend/models.py b/backend/models.py index 4a1657b..498c274 100644 --- a/backend/models.py +++ b/backend/models.py @@ -63,6 +63,7 @@ class Report(Base): closure_note = Column(Text) closed_at = Column(TIMESTAMP) closed_by = Column(Integer, ForeignKey("users.id"), nullable=True) + re_dispatch_count = Column(Integer, default=0) charger = relationship("Charger", back_populates="reports") photos = relationship("ReportPhoto", back_populates="report", cascade="all, delete-orphan") repair_links = relationship("RepairReport", back_populates="report") @@ -90,6 +91,8 @@ class Repair(Base): mechanic_lng = Column(Float) approved_at = Column(TIMESTAMP) approved_by = Column(Integer, ForeignKey("users.id")) + re_dispatch_requested = Column(Boolean, default=False) + re_dispatch_requested_at = Column(TIMESTAMP) mechanic = relationship("User", foreign_keys=[mechanic_id]) approver = relationship("User", foreign_keys=[approved_by]) report_links = relationship("RepairReport", back_populates="repair", cascade="all, delete-orphan") diff --git a/backend/routers/export.py b/backend/routers/export.py index 7d82545..02953a1 100644 --- a/backend/routers/export.py +++ b/backend/routers/export.py @@ -88,13 +88,14 @@ def export_reports(db: Session = Depends(get_db), _=Depends(require_admin)): "조치시작","조치완료","작업소요시간","신고→완료소요시간", "문제원인(관리자)","비고","출장비부담주체","출장비금액(원)","출장비상태", "처리담당자","처리일시","연결개선항목번호", - "상황종료사유","상황종료메모","상황종료일시","상황종료처리자" + "상황종료사유","상황종료메모","상황종료일시","상황종료처리자", + "재조치횟수" ] style_header(ws, headers) col_widths = [10,14,14,14,18,14,12,12,12,22,12,24,14,16,16,10,16,12, 12,14,16,24,16,16,12,18,24,24,16,12,12,12,16,18, - 18,24,16,14] + 18,24,16,14,10] for i, w in enumerate(col_widths, 1): ws.column_dimensions[ws.cell(1, i).column_letter].width = w @@ -148,6 +149,7 @@ def export_reports(db: Session = Depends(get_db), _=Depends(require_admin)): r.closure_note or "", fmt_dt(r.closed_at), r.closer.name if r.closer else "", + r.re_dispatch_count or 0, ] for col, val in enumerate(row_data, 1): ws.cell(row=row_num, column=col, value=val) diff --git a/backend/routers/repairs.py b/backend/routers/repairs.py index 8f8d752..e1270df 100644 --- a/backend/routers/repairs.py +++ b/backend/routers/repairs.py @@ -50,6 +50,8 @@ def _fmt_repair(repair: models.Repair) -> dict: "completed_at": repair.completed_at.isoformat() if repair.completed_at else None, "approved_at": repair.approved_at.isoformat() if repair.approved_at else None, "approved_by_name": repair.approver.name if repair.approved_by and repair.approver else None, + "re_dispatch_requested": repair.re_dispatch_requested or False, + "re_dispatch_requested_at": repair.re_dispatch_requested_at.isoformat() if repair.re_dispatch_requested_at else None, "photos_before": [{"id": p.id, "path": p.file_path} for p in repair.photos if p.photo_type == "before"], "photos_after": [{"id": p.id, "path": p.file_path} for p in repair.photos if p.photo_type == "after"], "reports": reports, @@ -67,9 +69,9 @@ def pending_reports(db: Session = Depends(get_db), result = [] for r in q.all(): c = r.charger - # in_progress 신고는 연결된 repair_id 포함 → 편집 모드로 연결 + # in_progress 신고만 기존 repair 편집 모드; pending은 재조치 포함 새 조치 생성 repair_id = None - if r.repair_links: + if r.status == "in_progress" and r.repair_links: repair_id = r.repair_links[0].repair_id result.append({ "id": r.id, "charger_id": r.charger_id, @@ -80,6 +82,7 @@ def pending_reports(db: Session = Depends(get_db), "reported_at": r.reported_at.isoformat(), "occurred_at": r.occurred_at.isoformat() if r.occurred_at else None, "repair_id": repair_id, + "re_dispatch_count": r.re_dispatch_count or 0, "gps_lat": c.gps_lat if c else None, "gps_lng": c.gps_lng if c else None, "location_detail": c.location_detail if c else None, @@ -263,6 +266,27 @@ def approve_repair( return {"ok": True, "improvement_id": target_imp_id} +@router.post("/{repair_id}/re-dispatch") +def re_dispatch_repair( + repair_id: int, + db: Session = Depends(get_db), + current_user: models.User = Depends(require_admin) +): + """기존 조치 기록을 유지하며 재조치 요청 — 신고를 pending으로 되돌림""" + repair = db.query(models.Repair).filter_by(id=repair_id).first() + if not repair: raise HTTPException(404) + if repair.approved_at: + raise HTTPException(400, "이미 승인된 조치는 재조치 요청할 수 없습니다.") + repair.re_dispatch_requested = True + repair.re_dispatch_requested_at = datetime.now() + for link in repair.report_links: + if link.report: + link.report.status = "pending" + link.report.re_dispatch_count = (link.report.re_dispatch_count or 0) + 1 + db.commit() + return {"ok": True} + + @router.delete("/{repair_id}") def cancel_repair( repair_id: int, diff --git a/backend/routers/reports.py b/backend/routers/reports.py index 59c58fb..323d493 100644 --- a/backend/routers/reports.py +++ b/backend/routers/reports.py @@ -203,11 +203,11 @@ def get_report(report_id: int, db: Session = Depends(get_db), r = db.query(models.Report).filter_by(id=report_id).first() if not r: raise HTTPException(404) result = _fmt_report(r, db) - # 수리 정보 포함 - if r.repair_links: - repair = r.repair_links[0].repair + result["re_dispatch_count"] = r.re_dispatch_count or 0 + # 수리 정보 포함 — repair_links를 id 내림차순(최신 우선)으로 정렬 + def _fmt_one_repair(repair, include_cost=True): cost = repair.cost - result["repair"] = { + return { "id": repair.id, "mechanic_name": repair.mechanic.name if repair.mechanic else None, "mechanic_company": repair.mechanic.company if repair.mechanic else None, @@ -222,6 +222,8 @@ def get_report(report_id: int, db: Session = Depends(get_db), "charger_lng": r.charger.gps_lng if r.charger else None, "approved_at": repair.approved_at.isoformat() if repair.approved_at else None, "approved_by_name": repair.approver.name if repair.approved_by and repair.approver else None, + "re_dispatch_requested": repair.re_dispatch_requested or False, + "re_dispatch_requested_at": repair.re_dispatch_requested_at.isoformat() if repair.re_dispatch_requested_at else None, "photos_before": [p.file_path for p in repair.photos if p.photo_type == "before"], "photos_after": [p.file_path for p in repair.photos if p.photo_type == "after"], "cost": { @@ -232,9 +234,19 @@ def get_report(report_id: int, db: Session = Depends(get_db), "cost_amount": cost.cost_amount, "cost_status": cost.cost_status, "manufacturer_name": cost.manufacturer.name if cost.manufacturer else None, - } if cost else None, - "linked_improvements": _get_linked_improvements(repair, db), + } if (cost and include_cost) else None, + "linked_improvements": _get_linked_improvements(repair, db) if include_cost else [], } + + if r.repair_links: + sorted_links = sorted(r.repair_links, key=lambda l: l.repair_id, reverse=True) + result["repair"] = _fmt_one_repair(sorted_links[0].repair) + # 재조치로 인한 이전 조치 이력 (최신 제외, re_dispatch_requested=True인 것) + result["prev_repairs"] = [ + _fmt_one_repair(link.repair, include_cost=False) + for link in sorted_links[1:] + if link.repair + ] return result def _get_linked_improvements(repair, db): diff --git a/frontend/static/pages/admin/report-detail.html b/frontend/static/pages/admin/report-detail.html index 9f0feaa..1a67973 100644 --- a/frontend/static/pages/admin/report-detail.html +++ b/frontend/static/pages/admin/report-detail.html @@ -206,6 +206,7 @@ async function load() { ]); const repair = r.repair; const cost = repair?.cost; + const prevRepairs = r.prev_repairs || []; document.getElementById('pageTitle').textContent = `신고 #${r.id} 상세`; @@ -529,8 +530,13 @@ async function load() {