From af7e47529c64c3f70e913454cf6e7658e8b58416 Mon Sep 17 00:00:00 2001 From: byun Date: Mon, 1 Jun 2026 09:39:50 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=E2=80=94=20=EC=8B=A0=EA=B3=A0=20=EC=83=81=ED=99=A9=EC=A2=85?= =?UTF-8?q?=EB=A3=8C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DB: reports 테이블에 closure_type, closure_note, closed_at, closed_by 컬럼 추가 - 백엔드: PATCH /reports/{id}/close 엔드포인트 (사유 4종: natural/remote_reset/false_alarm/other) - 신고상세: 승인대기 상태에서 [상황종료] 버튼 추가, 인라인 패널에서 사유 선택 - 상황종료 후 상세 화면에 사유·메모·처리자·일시 표시 - 엑셀 AS신고목록에 상황종료 4개 컬럼 추가 - 신고목록 필터·지도 상태 목록에 closed 추가, CSS 뱃지 추가 Co-Authored-By: Claude Sonnet 4.6 --- backend/models.py | 5 ++ backend/routers/export.py | 16 ++++- backend/routers/reports.py | 27 +++++++++ frontend/static/css/style.css | 1 + frontend/static/js/auth.js | 2 +- .../static/pages/admin/report-detail.html | 60 ++++++++++++++++++- frontend/static/pages/admin/reports.html | 3 +- 7 files changed, 108 insertions(+), 6 deletions(-) diff --git a/backend/models.py b/backend/models.py index a37271c..4a1657b 100644 --- a/backend/models.py +++ b/backend/models.py @@ -59,10 +59,15 @@ class Report(Base): source = Column(String(20), default="qr") # qr | admin reported_by = Column(Integer, ForeignKey("users.id"), nullable=True) reported_at = Column(TIMESTAMP, server_default=func.now()) + closure_type = Column(String(30)) # natural|remote_reset|false_alarm|other + closure_note = Column(Text) + closed_at = Column(TIMESTAMP) + closed_by = Column(Integer, ForeignKey("users.id"), nullable=True) charger = relationship("Charger", back_populates="reports") photos = relationship("ReportPhoto", back_populates="report", cascade="all, delete-orphan") repair_links = relationship("RepairReport", back_populates="report") reporter = relationship("User", foreign_keys=[reported_by]) + closer = relationship("User", foreign_keys=[closed_by]) class ReportPhoto(Base): __tablename__ = "report_photos" diff --git a/backend/routers/export.py b/backend/routers/export.py index df1f6c2..7d82545 100644 --- a/backend/routers/export.py +++ b/backend/routers/export.py @@ -74,6 +74,12 @@ def export_reports(db: Session = Depends(get_db), _=Depends(require_admin)): ws.title = "AS신고목록" ws.freeze_panes = "A2" + CLOSURE_LABEL = { + "natural": "증상자연소거", + "remote_reset": "원격리셋후증상소거", + "false_alarm": "인지오류", + "other": "기타", + } headers = [ "접수번호","충전기ID","충전기종류","충전기명","충전소명","CPO명","설치일", "신고위치(위도)","신고위치(경도)","문제유형","에러코드","상세설명", @@ -81,12 +87,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] + 12,14,16,24,16,16,12,18,24,24,16,12,12,12,16,18, + 18,24,16,14] for i, w in enumerate(col_widths, 1): ws.column_dimensions[ws.cell(1, i).column_letter].width = w @@ -136,6 +144,10 @@ def export_reports(db: Session = Depends(get_db), _=Depends(require_admin)): cost.reviewer.name if cost and cost.reviewer else "", fmt_dt(cost.reviewed_at) if cost else "", ", ".join(str(i) for i in imp_ids) if imp_ids else "", + CLOSURE_LABEL.get(r.closure_type, "") if r.closure_type else "", + r.closure_note or "", + fmt_dt(r.closed_at), + r.closer.name if r.closer else "", ] for col, val in enumerate(row_data, 1): ws.cell(row=row_num, column=col, value=val) diff --git a/backend/routers/reports.py b/backend/routers/reports.py index f6c3ab3..59c58fb 100644 --- a/backend/routers/reports.py +++ b/backend/routers/reports.py @@ -45,6 +45,10 @@ def _fmt_report(r: models.Report, db: Session): "repair_id": repair_id, "mechanic_name": mechanic_name, "mechanic_company": mechanic_company, + "closure_type": r.closure_type, + "closure_note": r.closure_note, + "closed_at": r.closed_at.isoformat() if r.closed_at else None, + "closed_by_name": r.closer.name if r.closer else None, } @router.post("") @@ -321,6 +325,29 @@ def approve_report(report_id: int, db: Session = Depends(get_db), _=Depends(requ r.status = "pending"; db.commit() return {"ok": True} +CLOSURE_TYPES = {"natural", "remote_reset", "false_alarm", "other"} + +@router.patch("/{report_id}/close") +def close_report( + report_id: int, + closure_type: str = Form(...), + closure_note: str = Form(""), + db: Session = Depends(get_db), + current_user: models.User = Depends(require_admin) +): + if closure_type not in CLOSURE_TYPES: + raise HTTPException(400, "유효하지 않은 상황종료 사유입니다.") + r = db.query(models.Report).filter_by(id=report_id).first() + if not r: raise HTTPException(404) + from datetime import datetime + r.status = "closed" + r.closure_type = closure_type + r.closure_note = closure_note.strip() or None + r.closed_at = datetime.now() + r.closed_by = current_user.id + db.commit() + return {"ok": True} + @router.patch("/{report_id}/status") def update_status(report_id: int, status: str = Form(...), db: Session = Depends(get_db), _=Depends(require_admin)): diff --git a/frontend/static/css/style.css b/frontend/static/css/style.css index 7592f6f..6344374 100644 --- a/frontend/static/css/style.css +++ b/frontend/static/css/style.css @@ -114,6 +114,7 @@ textarea{resize:vertical;min-height:80px;} .s-cost-billed{background:#DBEAFE;color:#1565C0;} .s-cost-waived{background:#F0F0F0;color:#555;} .s-cost-settled{background:#D1FAE5;color:#065F46;} +.s-closed{background:#F1F5F9;color:#475569;} /* ── ALERTS ── */ .alert{padding:12px 16px;border-radius:6px;margin-bottom:14px;font-size:13px;} diff --git a/frontend/static/js/auth.js b/frontend/static/js/auth.js index 58e4968..067ccde 100644 --- a/frontend/static/js/auth.js +++ b/frontend/static/js/auth.js @@ -73,7 +73,7 @@ const Auth = (() => { function statusBadge(status) { const map = { pending_approval: '승인대기', pending: '접수', in_progress: '처리중', - done: '완료', waiting: '부품대기', revisit: '재방문', + done: '완료', waiting: '부품대기', revisit: '재방문', closed: '상황종료', registered: '등록', reviewing: '검토중', developing: '개발중', deployed: '배포완료', }; diff --git a/frontend/static/pages/admin/report-detail.html b/frontend/static/pages/admin/report-detail.html index af5ddec..9f0feaa 100644 --- a/frontend/static/pages/admin/report-detail.html +++ b/frontend/static/pages/admin/report-detail.html @@ -395,8 +395,43 @@ async function load() {
${escHtmlDetail(r.ocpp_log)}
` : ''} ${r.status === 'pending_approval' ? ` - ` : ''} +
+ + +
+ ` : ''} + ${r.status === 'closed' ? ` +
+
🔚 상황종료 처리됨
+
사유: ${{'natural':'증상자연소거','remote_reset':'원격리셋후증상소거','false_alarm':'인지오류','other':'기타'}[r.closure_type]||r.closure_type||'-'}${r.closure_note ? ' — ' + escHtmlDetail(r.closure_note) : ''}
+
${r.closed_by_name||''} · ${r.closed_at ? new Date(r.closed_at).toLocaleString('ko-KR') : ''}
+
` : ''} @@ -886,6 +921,27 @@ async function approveReport(id) { load(); } +function toggleClosePanel() { + const panel = document.getElementById('closurePanel'); + if (!panel) return; + panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; +} + +async function submitClose(id) { + const selected = document.querySelector('input[name="closureType"]:checked'); + if (!selected) { alert('상황종료 사유를 선택해 주세요.'); return; } + const note = document.getElementById('closureNote')?.value || ''; + if (selected.value === 'other' && !note.trim()) { alert('기타 사유를 입력해 주세요.'); return; } + if (!confirm('상황종료 처리하시겠습니까?\n정비사 조치 없이 신고를 종결합니다.')) return; + try { + const fd = new FormData(); + fd.append('closure_type', selected.value); + fd.append('closure_note', note); + await API.patch(`/reports/${id}/close`, fd); + load(); + } catch(e) { alert('오류: ' + e.message); } +} + async function saveCost(repairId) { const partyType = document.getElementById('partyType').value; if (!partyType) { showCostErr('출장비 부담 주체를 선택해 주세요.'); return; } diff --git a/frontend/static/pages/admin/reports.html b/frontend/static/pages/admin/reports.html index 5be9362..a717baf 100644 --- a/frontend/static/pages/admin/reports.html +++ b/frontend/static/pages/admin/reports.html @@ -80,6 +80,7 @@ + @@ -245,7 +246,7 @@ const STATUS_CLASS = { pending: 'pending', pending_approval: 'pending', in_progress: 'in_progress', waiting: 'waiting', revisit: 'revisit', - done: 'done', + done: 'done', closed: 'done', }; // ── 지도 마커 렌더 ──