Files
ev-charger-as/backend/routers/chargers.py
byun 2e8751ea6c 기능 추가 및 버그 수정 — 처리시간 지표, 대시보드 차트, UI 개선
## 처리시간 지표
- 업무시간 기준(09-18 평일) / 공휴일 제외 24h / 달력 기준 3가지 모드 선택
- 공휴일 DB 관리 (holidays 테이블, 수동 등록·삭제·일괄 추가)
- 2026년 공휴일 등록 지원
- 설정 페이지에서 라디오 버튼으로 모드 선택

## 대시보드 차트
- 월별 평균 처리시간 막대 차트 추가
- 월별 신고 접수 건수 누적 막대 차트 추가
- 월별 → 일별 드릴다운 (막대 클릭 시 해당 월의 일별 차트로 전환)
- 일별 막대 클릭 시 처리 완료/신고 접수 상세 내역 모달
- 충전기별 누적 고장 건수 Top 10 수평 막대 차트 추가

## 신고 목록
- # 컬럼을 DB PK 대신 현재 목록 순서(1, 2, 3…)로 표시
- 엑셀 export 접수번호도 순차번호로 변경

## 모바일 네비게이션 버그 수정
- 모바일에서 가로 오버플로우 시 nav가 body 넓이로 늘어나
  햄버거 버튼이 화면 밖으로 밀리는 문제 수정
- nav를 position:fixed + body padding-top:54px 로 변경 (전체 페이지 적용)
- 충전기 관리·신고 목록 페이지 지도 컨테이너에 isolation:isolate 적용

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 06:52:56 +09:00

213 lines
9.6 KiB
Python

import os
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Body
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
from typing import List, Optional
from database import get_db
import models
from auth import require_admin, get_current_user
from utils import generate_qr
router = APIRouter(prefix="/api/chargers", tags=["chargers"])
# ── 충전기 종류 ──────────────────────────────────────
@router.get("/types")
def list_types(db: Session = Depends(get_db)):
types = db.query(models.ChargerType).order_by(models.ChargerType.id).all()
return [{"id": t.id, "name": t.name, "description": t.description,
"charger_count": len(t.chargers)} for t in types]
@router.post("/types")
def create_type(name: str = Form(...), description: str = Form(""),
db: Session = Depends(get_db), _=Depends(require_admin)):
t = models.ChargerType(name=name, description=description)
db.add(t); db.commit(); db.refresh(t)
return {"id": t.id, "name": t.name}
@router.put("/types/{type_id}")
def update_type(type_id: int, name: str = Form(...), description: str = Form(""),
db: Session = Depends(get_db), _=Depends(require_admin)):
t = db.query(models.ChargerType).filter_by(id=type_id).first()
if not t: raise HTTPException(404, "종류를 찾을 수 없습니다.")
t.name = name; t.description = description
db.commit()
return {"id": t.id, "name": t.name}
@router.delete("/types/{type_id}")
def delete_type(type_id: int, db: Session = Depends(get_db), _=Depends(require_admin)):
t = db.query(models.ChargerType).filter_by(id=type_id).first()
if not t: raise HTTPException(404)
if t.chargers: raise HTTPException(400, "해당 종류로 등록된 충전기가 있어 삭제할 수 없습니다.")
db.delete(t); db.commit()
return {"ok": True}
# ── 충전기 종류별 에러 코드 ──────────────────────────
@router.get("/types/{type_id}/errors")
def list_type_errors(type_id: int, db: Session = Depends(get_db)):
errors = (db.query(models.ChargerTypeError)
.filter_by(charger_type_id=type_id)
.order_by(models.ChargerTypeError.display_order)
.all())
return [{"id": e.id, "error_code": e.error_code, "error_name": e.error_name,
"range_condition": e.range_condition, "description": e.description,
"auto_recovery": e.auto_recovery, "display_order": e.display_order}
for e in errors]
@router.post("/types/{type_id}/errors")
def create_type_error(
type_id: int,
error_code: str = Form(...), error_name: str = Form(...),
range_condition: str = Form(""), description: str = Form(""),
auto_recovery: bool = Form(True), display_order: int = Form(0),
db: Session = Depends(get_db), _=Depends(require_admin)
):
if not db.query(models.ChargerType).filter_by(id=type_id).first():
raise HTTPException(404)
e = models.ChargerTypeError(
charger_type_id=type_id, error_code=error_code, error_name=error_name,
range_condition=range_condition or None, description=description or None,
auto_recovery=auto_recovery, display_order=display_order
)
db.add(e); db.commit(); db.refresh(e)
return {"id": e.id}
@router.put("/types/{type_id}/errors/{error_id}")
def update_type_error(
type_id: int, error_id: int,
error_code: str = Form(...), error_name: str = Form(...),
range_condition: str = Form(""), description: str = Form(""),
auto_recovery: bool = Form(True), display_order: int = Form(0),
db: Session = Depends(get_db), _=Depends(require_admin)
):
e = db.query(models.ChargerTypeError).filter_by(id=error_id, charger_type_id=type_id).first()
if not e: raise HTTPException(404)
e.error_code = error_code; e.error_name = error_name
e.range_condition = range_condition or None; e.description = description or None
e.auto_recovery = auto_recovery; e.display_order = display_order
db.commit()
return {"ok": True}
@router.delete("/types/{type_id}/errors/{error_id}")
def delete_type_error(
type_id: int, error_id: int,
db: Session = Depends(get_db), _=Depends(require_admin)
):
e = db.query(models.ChargerTypeError).filter_by(id=error_id, charger_type_id=type_id).first()
if not e: raise HTTPException(404)
db.delete(e); db.commit()
return {"ok": True}
# ── 충전기 ──────────────────────────────────────────
@router.delete("/bulk")
def bulk_delete_chargers(
ids: List[str] = Body(...),
db: Session = Depends(get_db),
_=Depends(require_admin)
):
if not ids:
raise HTTPException(400, "삭제할 항목을 선택하세요.")
has_reports = db.query(models.Report.charger_id).filter(
models.Report.charger_id.in_(ids)).distinct().all()
if has_reports:
blocked = [r[0] for r in has_reports]
raise HTTPException(400, f"신고 내역이 있는 충전기는 삭제할 수 없습니다: {', '.join(blocked)}")
result = db.query(models.Charger).filter(models.Charger.id.in_(ids)).delete(synchronize_session=False)
db.commit()
return {"deleted": result}
@router.get("")
def list_chargers(db: Session = Depends(get_db)):
chargers = db.query(models.Charger).order_by(models.Charger.id).all()
result = []
for c in chargers:
pending = db.query(models.Report).filter(
models.Report.charger_id == c.id,
models.Report.status.in_(["pending", "in_progress"])
).count()
result.append({
"id": c.id, "name": c.name, "station_name": c.station_name,
"cpo_name": c.cpo_name, "location_detail": c.location_detail,
"installed_at": str(c.installed_at) if c.installed_at else None,
"gps_lat": c.gps_lat, "gps_lng": c.gps_lng, "is_active": c.is_active,
"charger_type": c.charger_type.name if c.charger_type else None,
"charger_type_id": c.charger_type_id,
"pending_reports": pending,
})
return result
@router.get("/{charger_id}/errors")
def get_charger_errors(charger_id: str, db: Session = Depends(get_db)):
c = db.query(models.Charger).filter_by(id=charger_id).first()
if not c: raise HTTPException(404)
if not c.charger_type_id: return []
errors = (db.query(models.ChargerTypeError)
.filter_by(charger_type_id=c.charger_type_id)
.order_by(models.ChargerTypeError.display_order)
.all())
return [{"id": e.id, "error_code": e.error_code, "error_name": e.error_name,
"range_condition": e.range_condition, "auto_recovery": e.auto_recovery}
for e in errors]
@router.get("/{charger_id}")
def get_charger(charger_id: str, db: Session = Depends(get_db)):
c = db.query(models.Charger).filter_by(id=charger_id).first()
if not c: raise HTTPException(404, "충전기를 찾을 수 없습니다.")
return {
"id": c.id, "name": c.name, "station_name": c.station_name,
"cpo_name": c.cpo_name, "location_detail": c.location_detail,
"installed_at": str(c.installed_at) if c.installed_at else None,
"gps_lat": c.gps_lat, "gps_lng": c.gps_lng, "is_active": c.is_active,
"charger_type": c.charger_type.name if c.charger_type else None,
"charger_type_id": c.charger_type_id,
}
@router.post("")
def create_charger(
id: str = Form(...), charger_type_id: int = Form(...),
name: str = Form(...), station_name: str = Form(...),
location_detail: str = Form(""), cpo_name: str = Form(""),
installed_at: Optional[str] = Form(None),
gps_lat: Optional[float] = Form(None), gps_lng: Optional[float] = Form(None),
db: Session = Depends(get_db), _=Depends(require_admin)
):
if db.query(models.Charger).filter_by(id=id).first():
raise HTTPException(400, "이미 존재하는 충전기 ID입니다.")
c = models.Charger(
id=id, charger_type_id=charger_type_id, name=name,
station_name=station_name, location_detail=location_detail,
cpo_name=cpo_name, installed_at=installed_at or None,
gps_lat=gps_lat, gps_lng=gps_lng
)
db.add(c); db.commit()
domain = os.getenv("DOMAIN", "localhost")
qr_path = generate_qr(id, domain)
return {"id": c.id, "qr_path": qr_path}
@router.put("/{charger_id}")
def update_charger(
charger_id: str,
charger_type_id: int = Form(...), name: str = Form(...),
station_name: str = Form(...), location_detail: str = Form(""),
cpo_name: str = Form(""), installed_at: Optional[str] = Form(None),
gps_lat: Optional[float] = Form(None), gps_lng: Optional[float] = Form(None),
db: Session = Depends(get_db), _=Depends(require_admin)
):
c = db.query(models.Charger).filter_by(id=charger_id).first()
if not c: raise HTTPException(404)
c.charger_type_id = charger_type_id; c.name = name
c.station_name = station_name; c.location_detail = location_detail
c.cpo_name = cpo_name; c.installed_at = installed_at or None
c.gps_lat = gps_lat; c.gps_lng = gps_lng
db.commit()
domain = os.getenv("DOMAIN", "localhost")
qr_path = generate_qr(charger_id, domain)
return {"id": c.id, "qr_path": qr_path}
@router.post("/{charger_id}/qr")
def regenerate_qr(charger_id: str, db: Session = Depends(get_db), _=Depends(require_admin)):
c = db.query(models.Charger).filter_by(id=charger_id).first()
if not c: raise HTTPException(404)
domain = os.getenv("DOMAIN", "localhost")
qr_path = generate_qr(charger_id, domain)
return {"qr_path": qr_path}