기능 개선 — 사진 업로드, 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:
byun
2026-06-02 05:38:33 +09:00
parent 5ebd0a6ae7
commit 9f0f4326fe
18 changed files with 436 additions and 255 deletions

View File

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

View File

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

View File

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

View File

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