"""DB 모델 정의""" from datetime import datetime from enum import Enum as PyEnum from sqlalchemy import ( Column, Integer, BigInteger, String, Float, DateTime, Enum, ForeignKey, Text, Boolean, Index, ) from sqlalchemy.orm import relationship from sqlalchemy.sql import func from app.database import Base # ── 상태 Enum ── class ChargerStatus(str, PyEnum): AVAILABLE = "Available" CHARGING = "Charging" FAULTED = "Faulted" UNAVAILABLE = "Unavailable" RESERVED = "Reserved" class SessionStatus(str, PyEnum): PENDING = "pending" # 결제 대기 AUTHORIZED = "authorized" # 결제 승인, 충전 시작 대기 CHARGING = "charging" # 충전 중 COMPLETED = "completed" # 정상 종료 FAILED = "failed" # 실패 CANCELLED = "cancelled" # 취소 class PaymentStatus(str, PyEnum): PENDING = "pending" CONFIRMED = "confirmed" FAILED = "failed" REFUNDED = "refunded" PARTIAL_REFUND = "partial_refund" # ── 충전기 ── class Charger(Base): __tablename__ = "chargers" id = Column(Integer, primary_key=True, autoincrement=True) charge_box_id = Column(String(64), unique=True, nullable=False, index=True, comment="OCPP chargeBoxId (Steve 등록명)") name = Column(String(128), comment="표시 이름 (예: A동 주차장 1번)") location = Column(String(256), comment="설치 위치") connector_count = Column(Integer, default=1) power_kw = Column(Float, default=7.0, comment="충전 용량 kW") status = Column(Enum(ChargerStatus), default=ChargerStatus.UNAVAILABLE) last_heartbeat = Column(DateTime(timezone=True)) is_active = Column(Boolean, default=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) sessions = relationship("ChargingSession", back_populates="charger") def __repr__(self): return f"" # ── 충전 세션 ── class ChargingSession(Base): __tablename__ = "charging_sessions" __table_args__ = ( Index("ix_session_charger_status", "charger_id", "status"), Index("ix_session_created", "created_at"), ) id = Column(Integer, primary_key=True, autoincrement=True) session_uid = Column(String(64), unique=True, nullable=False, index=True, comment="고유 세션 ID (UUID)") charger_id = Column(Integer, ForeignKey("chargers.id"), nullable=False) connector_id = Column(Integer, default=1) # OCPP 트랜잭션 정보 ocpp_transaction_id = Column(Integer, comment="Steve에서 발급한 transactionId") id_tag = Column(String(64), comment="OCPP 인증 태그") # 전력량 (Wh 단위) meter_start = Column(BigInteger, comment="시작 전력량 Wh") meter_stop = Column(BigInteger, comment="종료 전력량 Wh") charged_wh = Column(BigInteger, default=0, comment="충전된 전력량 Wh") last_meter_value = Column(BigInteger, comment="마지막 MeterValues Wh") # 요금 electricity_cost = Column(Integer, default=0, comment="전기 원가 (원)") service_fee = Column(Integer, default=0, comment="서비스 수수료 (원)") total_bill = Column(Integer, default=0, comment="총 요금 (원)") # 시간 status = Column(Enum(SessionStatus), default=SessionStatus.PENDING) started_at = Column(DateTime(timezone=True)) stopped_at = Column(DateTime(timezone=True)) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) charger = relationship("Charger", back_populates="sessions") payment = relationship("Payment", back_populates="session", uselist=False) def __repr__(self): return f"" # ── 결제 ── class Payment(Base): __tablename__ = "payments" __table_args__ = ( Index("ix_payment_order", "order_id"), ) id = Column(Integer, primary_key=True, autoincrement=True) session_id = Column(Integer, ForeignKey("charging_sessions.id"), nullable=False) # 토스페이먼츠 order_id = Column(String(128), unique=True, nullable=False, comment="토스 주문 ID") payment_key = Column(String(256), comment="토스 paymentKey") amount = Column(Integer, nullable=False, comment="결제 요청 금액 (원)") actual_amount = Column(Integer, comment="실제 충전 후 정산 금액") status = Column(Enum(PaymentStatus), default=PaymentStatus.PENDING) method = Column(String(32), comment="결제 수단 (카드, 간편결제 등)") card_company = Column(String(32), comment="카드사 이름") confirmed_at = Column(DateTime(timezone=True)) refunded_at = Column(DateTime(timezone=True)) created_at = Column(DateTime(timezone=True), server_default=func.now()) session = relationship("ChargingSession", back_populates="payment") def __repr__(self): return f"" # ── 충전기 MeterValue 로그 ── class MeterValueLog(Base): """실시간 MeterValues 로그 (디버깅/분석용)""" __tablename__ = "meter_value_logs" id = Column(BigInteger, primary_key=True, autoincrement=True) charger_id = Column(Integer, ForeignKey("chargers.id"), nullable=False) transaction_id = Column(Integer) connector_id = Column(Integer, default=1) measurand = Column(String(64), default="Energy.Active.Import.Register") value = Column(Float, nullable=False, comment="측정값") unit = Column(String(16), default="Wh") timestamp = Column(DateTime(timezone=True), server_default=func.now()) # ── 사용자 ── class UserRole(str, PyEnum): ADMIN = "admin" OPERATOR = "operator" VIEWER = "viewer" class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True, autoincrement=True) username = Column(String(64), unique=True, nullable=False, index=True) hashed_password = Column(String(256), nullable=False) display_name = Column(String(128), comment="표시 이름") role = Column(Enum(UserRole), default=UserRole.VIEWER) is_active = Column(Boolean, default=True) last_login = Column(DateTime(timezone=True)) created_at = Column(DateTime(timezone=True), server_default=func.now()) def __repr__(self): return f""