기능 추가 — 재조치 요청 (조치 기록 유지 재출동)

정비사 조치 완료 후 동일 문제 재발 시 관리자가 기존 기록을 보존한 채
재조치를 요청할 수 있는 기능 추가.

- DB: repairs.re_dispatch_requested/at, reports.re_dispatch_count 컬럼 추가
- 재조치 요청 엔드포인트 (POST /repairs/{id}/re-dispatch): 기존 repair에 플래그,
  연결 신고를 pending으로 복원, re_dispatch_count 증가
- pending 상태 신고는 새 조치 생성으로 분기 (in_progress만 기존 수정 모드)
- report-detail: 조치승인·취소 사이에 "🔁 재조치 요청" 버튼, 이전 조치 이력 카드
- 정비사 대시보드: 재조치 건에 🔁 뱃지 및 강조 버튼색 표시
- 엑셀 export: 재조치횟수 컬럼 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
byun
2026-06-01 09:58:50 +09:00
parent af7e47529c
commit 81c3428aa1
6 changed files with 99 additions and 13 deletions

View File

@@ -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")

View File

@@ -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)

View File

@@ -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,

View File

@@ -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):