Files
ev-charger-as/backend/auth.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

90 lines
3.0 KiB
Python

import os
import bcrypt
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from database import get_db
import models
SECRET_KEY = os.getenv("SECRET_KEY", "changeme")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_HOURS = 24
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
oauth2_scheme_optional = OAuth2PasswordBearer(tokenUrl="/api/auth/login", auto_error=False)
def hash_password(password: str) -> str:
"""비밀번호 bcrypt 해시 생성"""
salt = bcrypt.gensalt(rounds=12)
return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8")
def verify_password(plain: str, hashed: str) -> bool:
"""비밀번호 검증"""
try:
return bcrypt.checkpw(plain.encode("utf-8"), hashed.encode("utf-8"))
except Exception:
return False
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def get_current_user(
token: str = Depends(oauth2_scheme),
db: Session = Depends(get_db)
) -> models.User:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="인증 정보가 유효하지 않습니다.",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id: str = payload.get("sub")
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = db.query(models.User).filter(
models.User.id == int(user_id),
models.User.is_active == True
).first()
if user is None:
raise credentials_exception
return user
def get_optional_user(
token: Optional[str] = Depends(oauth2_scheme_optional),
db: Session = Depends(get_db)
) -> Optional[models.User]:
if not token:
return None
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id = payload.get("sub")
if not user_id:
return None
return db.query(models.User).filter(
models.User.id == int(user_id),
models.User.is_active == True
).first()
except Exception:
return None
def require_role(*roles):
def checker(current_user: models.User = Depends(get_current_user)):
if current_user.role not in roles:
raise HTTPException(status_code=403, detail="권한이 없습니다.")
return current_user
return checker
require_admin = require_role("admin")
require_mechanic = require_role("mechanic", "admin")
require_manufacturer = require_role("manufacturer", "admin")