Files
ev-charger-as/backend/routers/export.py
2026-04-18 06:18:58 +09:00

246 lines
11 KiB
Python

from fastapi import APIRouter, Depends
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from sqlalchemy import desc
from io import BytesIO
from datetime import datetime
from urllib.parse import quote
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from database import get_db
import models
from auth import require_admin
router = APIRouter(prefix="/api/export", tags=["export"])
NAVY = "0B1E3D"
LIGHT = "D6EAF8"
def style_header(ws, headers, row=1):
bd = Side(style="thin", color="AAAAAA")
for col, h in enumerate(headers, 1):
cell = ws.cell(row=row, column=col, value=h)
cell.font = Font(bold=True, color="FFFFFF", size=11)
cell.fill = PatternFill("solid", fgColor=NAVY)
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
cell.border = Border(left=bd, right=bd, top=bd, bottom=bd)
ws.row_dimensions[row].height = 20
def style_row(ws, row_num, num_cols, even=True):
bd = Side(style="thin", color="DDDDDD")
for col in range(1, num_cols + 1):
cell = ws.cell(row=row_num, column=col)
if even:
cell.fill = PatternFill("solid", fgColor="F4F7FB")
cell.border = Border(left=bd, right=bd, top=bd, bottom=bd)
cell.alignment = Alignment(vertical="center", wrap_text=True)
def fmt_dt(dt):
return dt.strftime("%Y-%m-%d %H:%M") if dt else ""
def fmt_d(d):
return str(d) if d else ""
def elapsed(start, end):
if not start or not end: return ""
diff = end - start
total = int(diff.total_seconds())
h, m = divmod(total // 60, 60)
return f"{h}시간 {m}"
def make_response(wb: openpyxl.Workbook, korean_name: str) -> StreamingResponse:
"""엑셀 파일을 StreamingResponse로 반환 — 한글 파일명 URL 인코딩 처리"""
buf = BytesIO()
wb.save(buf)
buf.seek(0)
date_str = datetime.now().strftime("%Y%m%d_%H%M")
filename = f"{korean_name}_{date_str}.xlsx"
encoded = quote(filename, safe="") # 한글 URL 인코딩
cd_header = f"attachment; filename*=UTF-8''{encoded}"
return StreamingResponse(
buf,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": cd_header},
)
# ─────────────────────────────────────────────
# 1. AS 신고 목록
# ─────────────────────────────────────────────
@router.get("/reports")
def export_reports(db: Session = Depends(get_db), _=Depends(require_admin)):
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "AS신고목록"
ws.freeze_panes = "A2"
headers = [
"접수번호","충전기ID","충전기종류","충전기명","충전소명","CPO명","설치일",
"신고위치(위도)","신고위치(경도)","문제유형","에러코드","상세설명",
"신고자연락처","문제발생시각","신고일시","처리상태",
"담당정비사","정비사소속","조치유형","조치내용",
"조치시작","조치완료","작업소요시간","신고→완료소요시간",
"문제원인(관리자)","비고","출장비부담주체","출장비금액(원)","출장비상태",
"처리담당자","처리일시","연결개선항목번호"
]
style_header(ws, headers)
col_widths = [10,14,14,14,18,14,12,12,12,22,12,24,14,16,16,12,
12,14,16,24,16,16,12,18,24,24,16,12,12,12,16,18]
for i, w in enumerate(col_widths, 1):
ws.column_dimensions[ws.cell(1, i).column_letter].width = w
reports = db.query(models.Report).order_by(desc(models.Report.reported_at)).all()
for row_num, r in enumerate(reports, 2):
c = r.charger
repair = r.repair_links[0].repair if r.repair_links else None
cost = repair.cost if repair else None
imp_ids = [
ir.improvement_id
for ir in db.query(models.ImprovementReport).filter_by(report_id=r.id).all()
]
row_data = [
r.id,
r.charger_id,
c.charger_type.name if c and c.charger_type else "",
c.name if c else "",
c.station_name if c else "",
c.cpo_name if c else "",
fmt_d(c.installed_at) if c else "",
r.gps_lat or "",
r.gps_lng or "",
", ".join(r.issue_types) if r.issue_types else "",
r.error_code or "",
r.issue_detail or "",
r.contact or "",
fmt_dt(r.occurred_at),
fmt_dt(r.reported_at),
r.status,
repair.mechanic.name if repair and repair.mechanic else "",
repair.mechanic.company if repair and repair.mechanic else "",
", ".join(repair.repair_types) if repair and repair.repair_types else "",
repair.description if repair else "",
fmt_dt(repair.started_at) if repair else "",
fmt_dt(repair.completed_at) if repair else "",
elapsed(repair.started_at, repair.completed_at) if repair else "",
elapsed(r.occurred_at or r.reported_at, repair.completed_at if repair else None),
cost.root_cause if cost else "",
cost.admin_note if cost else "",
cost.cost_party_type if cost else "",
cost.cost_amount if cost else "",
cost.cost_status if cost else "",
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 "",
]
for col, val in enumerate(row_data, 1):
ws.cell(row=row_num, column=col, value=val)
style_row(ws, row_num, len(headers), row_num % 2 == 0)
ws.row_dimensions[row_num].height = 16
return make_response(wb, "AS신고목록")
# ─────────────────────────────────────────────
# 2. 출장비 목록
# ─────────────────────────────────────────────
@router.get("/costs")
def export_costs(db: Session = Depends(get_db), _=Depends(require_admin)):
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "출장비목록"
ws.freeze_panes = "A2"
headers = [
"신고번호","충전기ID","충전기종류","충전소명","조치완료일",
"정비사","소속","문제원인","비고",
"출장비부담주체","제조사명","금액(원)","처리상태",
"처리담당자","처리일시"
]
style_header(ws, headers)
for i, w in enumerate([10,14,14,18,16,12,14,24,24,16,16,12,12,12,16], 1):
ws.column_dimensions[ws.cell(1, i).column_letter].width = w
costs = db.query(models.RepairCost).join(models.Repair).order_by(
desc(models.RepairCost.reviewed_at)).all()
for row_num, cost in enumerate(costs, 2):
repair = cost.repair
rids = [rr.report_id for rr in repair.report_links]
charger_id = station_name = charger_type = ""
if rids:
r = db.query(models.Report).filter_by(id=rids[0]).first()
if r and r.charger:
charger_id = r.charger_id
station_name = r.charger.station_name
charger_type = r.charger.charger_type.name if r.charger.charger_type else ""
row_data = [
", ".join(str(i) for i in rids),
charger_id, charger_type, station_name,
fmt_dt(repair.completed_at),
repair.mechanic.name if repair.mechanic else "",
repair.mechanic.company if repair.mechanic else "",
cost.root_cause or "",
cost.admin_note or "",
cost.cost_party_type or "",
cost.manufacturer.company if cost.manufacturer else (cost.cost_party_custom or ""),
cost.cost_amount or 0,
cost.cost_status or "",
cost.reviewer.name if cost.reviewer else "",
fmt_dt(cost.reviewed_at),
]
for col, val in enumerate(row_data, 1):
ws.cell(row=row_num, column=col, value=val)
style_row(ws, row_num, len(headers), row_num % 2 == 0)
ws.row_dimensions[row_num].height = 16
return make_response(wb, "출장비목록")
# ─────────────────────────────────────────────
# 3. 개선항목 목록
# ─────────────────────────────────────────────
@router.get("/improvements")
def export_improvements(db: Session = Depends(get_db), _=Depends(require_admin)):
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "개선항목목록"
ws.freeze_panes = "A2"
headers = [
"번호","제목","분류","우선순위","개선내용","관련부품",
"담당제조사","담당자","연락처","연결AS건수","연결AS번호",
"진행상태","SW배포목표일","SW실제배포일","제조사메모",
"등록관리자","등록일시"
]
style_header(ws, headers)
for i, w in enumerate([8,24,10,10,30,14,16,12,14,10,18,12,14,14,24,12,16], 1):
ws.column_dimensions[ws.cell(1, i).column_letter].width = w
imps = db.query(models.Improvement).order_by(desc(models.Improvement.created_at)).all()
for row_num, imp in enumerate(imps, 2):
rids = [ir.report_id for ir in imp.report_links]
row_data = [
imp.id, imp.title, imp.category, imp.priority,
imp.description, imp.part_name or "",
imp.manufacturer.company if imp.manufacturer else "",
imp.manufacturer.name if imp.manufacturer else "",
imp.manufacturer.phone if imp.manufacturer else "",
len(rids),
", ".join(str(i) for i in rids),
imp.status,
fmt_d(imp.sw_deploy_target),
fmt_d(imp.sw_deployed_at),
imp.manufacturer_memo or "",
imp.creator.name if imp.creator else "",
fmt_dt(imp.created_at),
]
for col, val in enumerate(row_data, 1):
ws.cell(row=row_num, column=col, value=val)
style_row(ws, row_num, len(headers), row_num % 2 == 0)
ws.row_dimensions[row_num].height = 16
return make_response(wb, "개선항목목록")