기능 개선 — 사진 업로드, HEIC 지원, 재조치 흐름, 신고 순번, 모바일 UI
- 이미지 압축: 삼성/네이버 브라우저 호환, URL.createObjectURL 방식으로 메모리 절감, 대용량 PNG/HEIC 처리, blob 유효성 검증, 순차 압축으로 모바일 OOM 방지 - HEIC/HEIF 지원: pillow-heif 서버사이드 변환, Pillow 12.2.0 업그레이드 - 조치 페이지: '조치 완료 저장' 단일 버튼으로 단순화 - 재조치 흐름: 관리자 재조치 요청 시 이전 조치 이력을 번호 카드로 순차 표시 - 신고 순번: 전체 기준 ROW_NUMBER(oldest=1) 순번 표시, 삭제 gap 제거 - 모바일 탭바: position:fixed 적용으로 nav 하단 흰 여백 제거 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,8 @@ bcrypt==4.0.1
|
||||
passlib[bcrypt]==1.7.4
|
||||
python-multipart==0.0.9
|
||||
qrcode[pil]==7.4.2
|
||||
Pillow==10.3.0
|
||||
Pillow==12.2.0
|
||||
pillow-heif==1.3.0
|
||||
openpyxl==3.1.2
|
||||
python-dotenv==1.0.1
|
||||
pydantic[email]==2.7.1
|
||||
|
||||
@@ -112,6 +112,7 @@ def open_reports_for_charger(charger_id: str, db: Session = Depends(get_db),
|
||||
"issue_detail": r.issue_detail, "status": r.status,
|
||||
"reported_at": r.reported_at.isoformat(),
|
||||
"photos": [p.file_path for p in r.photos],
|
||||
"re_dispatch_count": r.re_dispatch_count or 0,
|
||||
} for r in reports]
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Body
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc, text
|
||||
from sqlalchemy import desc, text, func
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
import os, uuid
|
||||
@@ -185,17 +185,32 @@ def list_reports(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
q = db.query(models.Report).order_by(desc(models.Report.reported_at))
|
||||
# 전체 신고 기준 순번 (삭제 gap 없이, 오래된 것=1)
|
||||
seq_subq = db.query(
|
||||
models.Report.id.label("rid"),
|
||||
func.row_number().over(
|
||||
order_by=[models.Report.reported_at.asc(), models.Report.id.asc()]
|
||||
).label("seq")
|
||||
).subquery()
|
||||
|
||||
q = (db.query(models.Report, seq_subq.c.seq)
|
||||
.join(seq_subq, models.Report.id == seq_subq.c.rid)
|
||||
.order_by(desc(models.Report.reported_at)))
|
||||
if status:
|
||||
q = q.filter(models.Report.status == status)
|
||||
elif active_only:
|
||||
q = q.filter(models.Report.status.in_(
|
||||
["pending", "pending_approval", "in_progress", "waiting", "revisit"]))
|
||||
if charger_id: q = q.filter(models.Report.charger_id == charger_id)
|
||||
# 정비사는 공개된 것만 (승인 대기 제외)
|
||||
if current_user.role == "mechanic":
|
||||
q = q.filter(models.Report.status != "pending_approval")
|
||||
return [_fmt_report(r, db) for r in q.all()]
|
||||
|
||||
result = []
|
||||
for r, seq in q.all():
|
||||
fmt = _fmt_report(r, db)
|
||||
fmt["seq"] = seq
|
||||
result.append(fmt)
|
||||
return result
|
||||
|
||||
@router.get("/{report_id}")
|
||||
def get_report(report_id: int, db: Session = Depends(get_db),
|
||||
@@ -204,6 +219,15 @@ def get_report(report_id: int, db: Session = Depends(get_db),
|
||||
if not r: raise HTTPException(404)
|
||||
result = _fmt_report(r, db)
|
||||
result["re_dispatch_count"] = r.re_dispatch_count or 0
|
||||
# 전체 기준 순번 계산
|
||||
seq_subq = db.query(
|
||||
models.Report.id.label("rid"),
|
||||
func.row_number().over(
|
||||
order_by=[models.Report.reported_at.asc(), models.Report.id.asc()]
|
||||
).label("seq")
|
||||
).subquery()
|
||||
row = db.query(seq_subq.c.seq).filter(seq_subq.c.rid == report_id).scalar()
|
||||
result["seq"] = row
|
||||
# 수리 정보 포함 — repair_links를 id 내림차순(최신 우선)으로 정렬
|
||||
def _fmt_one_repair(repair, include_cost=True):
|
||||
cost = repair.cost
|
||||
|
||||
@@ -1,18 +1,47 @@
|
||||
import os, uuid, qrcode
|
||||
import os, uuid, io, qrcode
|
||||
from PIL import Image
|
||||
from fastapi import UploadFile
|
||||
|
||||
# HEIC/HEIF 지원 등록 (pillow-heif)
|
||||
try:
|
||||
from pillow_heif import register_heif_opener
|
||||
register_heif_opener()
|
||||
_HEIF_SUPPORTED = True
|
||||
except ImportError:
|
||||
_HEIF_SUPPORTED = False
|
||||
|
||||
UPLOAD_DIR = os.getenv("UPLOAD_DIR", "/uploads")
|
||||
|
||||
# 브라우저에서 표시 불가능한 포맷 → 서버에서 JPEG 변환
|
||||
_CONVERT_EXTS = {".heic", ".heif", ".avif"}
|
||||
|
||||
def save_upload(file: UploadFile, sub_dir: str = "general") -> str:
|
||||
"""파일을 저장하고 /uploads 기준 상대 경로 반환"""
|
||||
"""파일을 저장하고 /uploads 기준 상대 경로 반환.
|
||||
HEIC/HEIF 등 브라우저 비호환 포맷은 JPEG로 변환 후 저장."""
|
||||
raw = file.file.read()
|
||||
ext = os.path.splitext(file.filename or "file")[1].lower() or ".jpg"
|
||||
folder = os.path.join(UPLOAD_DIR, sub_dir)
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
|
||||
if ext in _CONVERT_EXTS:
|
||||
# HEIC/HEIF → JPEG 변환
|
||||
try:
|
||||
img = Image.open(io.BytesIO(raw))
|
||||
if img.mode in ("RGBA", "P", "LA"):
|
||||
img = img.convert("RGB")
|
||||
elif img.mode != "RGB":
|
||||
img = img.convert("RGB")
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="JPEG", quality=88, optimize=True)
|
||||
raw = buf.getvalue()
|
||||
ext = ".jpg"
|
||||
except Exception:
|
||||
pass # 변환 실패 시 원본 그대로 저장
|
||||
|
||||
filename = f"{uuid.uuid4().hex}{ext}"
|
||||
filepath = os.path.join(folder, filename)
|
||||
with open(filepath, "wb") as f:
|
||||
f.write(file.file.read())
|
||||
f.write(raw)
|
||||
return f"/uploads/{sub_dir}/{filename}"
|
||||
|
||||
def generate_qr(charger_id: str, domain: str) -> str:
|
||||
|
||||
Reference in New Issue
Block a user