Initial commit: AutonetSellCar platform with deployment system
- Frontend: Next.js 14 with TypeScript - Backend: FastAPI with SQLAlchemy - Agent: Carmodoo sync agent - Deployment: Docker Compose based staging/production setup - Scripts: Automated deployment with rollback support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
64
backend/app/models/__init__.py
Normal file
64
backend/app/models/__init__.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from .car import CarMaker, CarModel, Car, CarImage, CarOption
|
||||
from .user import User, CarView, PerformanceCheckView, ChargeHistory, VerificationCode
|
||||
from .inquiry import Inquiry, InquiryMessage, InquiryStatus, InquiryCategory
|
||||
from .hero_banner import HeroBanner, HeroBannerSettings
|
||||
from .translation import Translation
|
||||
from .cache import CarCache, CarDetailCache, CacheRequestQueue
|
||||
from .settings import SystemSettings
|
||||
from .vehicle_request import VehicleRequest, RequestVehicle, PurchasedVehicle
|
||||
from .dealer import DealerApplication, DealerInfo
|
||||
from .vehicle_share import VehicleShare, ShareReward
|
||||
from .withdrawal import WithdrawalRequest
|
||||
from .referral import ReferralReward
|
||||
from .notification import Notification
|
||||
from .push_subscription import PushSubscription, UserNotificationPreference
|
||||
from .performance_check import CarPerformanceCheck
|
||||
from .car_specification import CarSpecification
|
||||
from .exchange_rate import ExchangeRate, ExchangeRateHistory
|
||||
from .cc_package import CCPackage, DEFAULT_CC_PACKAGES
|
||||
from .visitor import VisitorLog, VisitorDailyStats, VisitorSession
|
||||
|
||||
__all__ = [
|
||||
"CarMaker",
|
||||
"CarModel",
|
||||
"Car",
|
||||
"CarImage",
|
||||
"CarOption",
|
||||
"CarPerformanceCheck",
|
||||
"CarSpecification",
|
||||
"User",
|
||||
"CarView",
|
||||
"PerformanceCheckView",
|
||||
"ChargeHistory",
|
||||
"VerificationCode",
|
||||
"Inquiry",
|
||||
"InquiryMessage",
|
||||
"InquiryStatus",
|
||||
"InquiryCategory",
|
||||
"HeroBanner",
|
||||
"HeroBannerSettings",
|
||||
"Translation",
|
||||
"CarCache",
|
||||
"CarDetailCache",
|
||||
"CacheRequestQueue",
|
||||
"SystemSettings",
|
||||
"VehicleRequest",
|
||||
"RequestVehicle",
|
||||
"PurchasedVehicle",
|
||||
"DealerApplication",
|
||||
"DealerInfo",
|
||||
"VehicleShare",
|
||||
"ShareReward",
|
||||
"WithdrawalRequest",
|
||||
"ReferralReward",
|
||||
"Notification",
|
||||
"PushSubscription",
|
||||
"UserNotificationPreference",
|
||||
"ExchangeRate",
|
||||
"ExchangeRateHistory",
|
||||
"CCPackage",
|
||||
"DEFAULT_CC_PACKAGES",
|
||||
"VisitorLog",
|
||||
"VisitorDailyStats",
|
||||
"VisitorSession",
|
||||
]
|
||||
75
backend/app/models/cache.py
Normal file
75
backend/app/models/cache.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
캐시 모델 - 카모두 검색 결과 캐싱
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Text, Index
|
||||
from sqlalchemy.sql import func
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class CarCache(Base):
|
||||
"""
|
||||
검색 결과 캐시 테이블 (Maker + Model 단위)
|
||||
캐시 키: maker_code_model_code (예: "2_38" = 기아_K5)
|
||||
"""
|
||||
__tablename__ = "car_cache"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
cache_key = Column(String(50), unique=True, nullable=False, index=True)
|
||||
|
||||
maker_code = Column(String(10), nullable=False)
|
||||
maker_name = Column(String(100), nullable=False)
|
||||
model_code = Column(String(10), nullable=False)
|
||||
model_name = Column(String(100), nullable=False)
|
||||
|
||||
total_count = Column(Integer, nullable=False, default=0)
|
||||
cars_data = Column(Text, nullable=False) # JSON: 전체 차량 목록
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
expires_at = Column(DateTime(timezone=True), nullable=False)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_car_cache_expires', 'expires_at'),
|
||||
Index('idx_car_cache_maker_model', 'maker_code', 'model_code'),
|
||||
)
|
||||
|
||||
|
||||
class CarDetailCache(Base):
|
||||
"""
|
||||
개별 차량 상세 정보 캐시 테이블
|
||||
"""
|
||||
__tablename__ = "car_detail_cache"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
car_id = Column(String(50), unique=True, nullable=False, index=True) # 카모두 차량 ID
|
||||
|
||||
detail_data = Column(Text, nullable=False) # JSON: 상세 정보
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
expires_at = Column(DateTime(timezone=True), nullable=False)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_car_detail_cache_expires', 'expires_at'),
|
||||
)
|
||||
|
||||
|
||||
class CacheRequestQueue(Base):
|
||||
"""
|
||||
캐시 요청 대기열 - 동일 조건 요청 병합용
|
||||
"""
|
||||
__tablename__ = "cache_request_queue"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
cache_key = Column(String(50), nullable=False, index=True)
|
||||
status = Column(String(20), nullable=False, default='pending') # pending, processing, completed, failed
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
started_at = Column(DateTime(timezone=True))
|
||||
completed_at = Column(DateTime(timezone=True))
|
||||
|
||||
error_message = Column(Text)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_cache_request_status', 'status', 'cache_key'),
|
||||
)
|
||||
110
backend/app/models/car.py
Normal file
110
backend/app/models/car.py
Normal file
@@ -0,0 +1,110 @@
|
||||
from sqlalchemy import Column, Integer, String, BigInteger, Boolean, ForeignKey, DateTime, Text, DECIMAL
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class CarMaker(Base):
|
||||
__tablename__ = "car_makers"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
code = Column(String(10), unique=True, nullable=False, index=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
name_en = Column(String(100))
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
models = relationship("CarModel", back_populates="maker")
|
||||
cars = relationship("Car", back_populates="maker")
|
||||
|
||||
|
||||
class CarModel(Base):
|
||||
__tablename__ = "car_models"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
code = Column(String(10), nullable=False, index=True)
|
||||
maker_id = Column(Integer, ForeignKey("car_makers.id"), nullable=False)
|
||||
name = Column(String(100), nullable=False)
|
||||
name_en = Column(String(100))
|
||||
|
||||
maker = relationship("CarMaker", back_populates="models")
|
||||
cars = relationship("Car", back_populates="model")
|
||||
|
||||
|
||||
class Car(Base):
|
||||
__tablename__ = "cars"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
source = Column(String(50), nullable=False, default="carmodoo")
|
||||
source_id = Column(String(50), nullable=False, index=True)
|
||||
source_key = Column(Text)
|
||||
|
||||
maker_id = Column(Integer, ForeignKey("car_makers.id"))
|
||||
model_id = Column(Integer, ForeignKey("car_models.id"))
|
||||
car_name = Column(String(200))
|
||||
|
||||
year = Column(Integer, index=True)
|
||||
month = Column(Integer)
|
||||
mileage = Column(Integer)
|
||||
price_krw = Column(BigInteger, index=True)
|
||||
margin_krw = Column(BigInteger, default=0) # Korean margin amount in KRW
|
||||
margin_mn = Column(BigInteger, default=0) # Mongolian margin amount in KRW
|
||||
price_usd = Column(DECIMAL(12, 2))
|
||||
is_displayed = Column(Boolean, default=False, index=True) # Show to users
|
||||
|
||||
fuel = Column(String(20))
|
||||
transmission = Column(String(20))
|
||||
color = Column(String(50))
|
||||
displacement = Column(Integer)
|
||||
car_number = Column(String(20))
|
||||
|
||||
seize_count = Column(Integer, default=0)
|
||||
collateral_count = Column(Integer, default=0)
|
||||
|
||||
check_num = Column(String(50))
|
||||
dealer_name = Column(String(100))
|
||||
dealer_phone = Column(String(50))
|
||||
shop_name = Column(String(100))
|
||||
dealer_description = Column(Text) # 딜러가 작성한 차량 상세설명 (한국어 원문)
|
||||
dealer_description_en = Column(Text) # 영어 번역
|
||||
dealer_description_mn = Column(Text) # 몽골어 번역
|
||||
dealer_description_ru = Column(Text) # 러시아어 번역
|
||||
|
||||
memo = Column(Text)
|
||||
|
||||
status = Column(String(20), default="active", index=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
synced_at = Column(DateTime(timezone=True))
|
||||
|
||||
maker = relationship("CarMaker", back_populates="cars")
|
||||
model = relationship("CarModel", back_populates="cars")
|
||||
images = relationship("CarImage", back_populates="car", cascade="all, delete-orphan")
|
||||
options = relationship("CarOption", back_populates="car", cascade="all, delete-orphan")
|
||||
# inquiries relationship disabled due to schema mismatch - use raw SQL for inquiry operations
|
||||
# inquiries = relationship("Inquiry", back_populates="car")
|
||||
views = relationship("CarView", back_populates="car", cascade="all, delete-orphan")
|
||||
performance_check = relationship("CarPerformanceCheck", back_populates="car", uselist=False, cascade="all, delete-orphan")
|
||||
specification = relationship("CarSpecification", back_populates="car", uselist=False, cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class CarImage(Base):
|
||||
__tablename__ = "car_images"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
car_id = Column(Integer, ForeignKey("cars.id", ondelete="CASCADE"), nullable=False)
|
||||
url = Column(String(500))
|
||||
local_path = Column(String(500))
|
||||
is_main = Column(Boolean, default=False)
|
||||
sort_order = Column(Integer, default=0)
|
||||
|
||||
car = relationship("Car", back_populates="images")
|
||||
|
||||
|
||||
class CarOption(Base):
|
||||
__tablename__ = "car_options"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
car_id = Column(Integer, ForeignKey("cars.id", ondelete="CASCADE"), nullable=False)
|
||||
option_name = Column(String(100))
|
||||
|
||||
car = relationship("Car", back_populates="options")
|
||||
59
backend/app/models/car_specification.py
Normal file
59
backend/app/models/car_specification.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
차량 상세사양 (Car Specifications) 모델
|
||||
카모두 상세사양조회 서비스에서 가져온 차량 스펙 정보를 저장
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, Text, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class CarSpecification(Base):
|
||||
"""차량 상세사양"""
|
||||
__tablename__ = "car_specifications"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
car_id = Column(Integer, ForeignKey("cars.id", ondelete="CASCADE"), nullable=False, unique=True)
|
||||
|
||||
# 기본 정보
|
||||
manufacturer = Column(String(50)) # 제조사
|
||||
model_name = Column(String(100)) # 모델명
|
||||
grade = Column(String(100)) # 등급/트림
|
||||
model_year = Column(String(20)) # 연식
|
||||
|
||||
# 엔진/성능
|
||||
displacement = Column(Integer) # 배기량 (cc)
|
||||
fuel_type = Column(String(30)) # 연료 (가솔린/디젤/하이브리드/전기)
|
||||
transmission = Column(String(30)) # 변속기 (자동/수동/CVT)
|
||||
drive_type = Column(String(30)) # 구동방식 (전륜/후륜/4륜)
|
||||
max_power = Column(String(50)) # 최고출력 (예: 180ps/6,000rpm)
|
||||
max_torque = Column(String(50)) # 최대토크 (예: 23.5kg.m/4,200rpm)
|
||||
fuel_efficiency = Column(String(50)) # 연비 (예: 12.5km/L)
|
||||
|
||||
# 차체
|
||||
body_type = Column(String(30)) # 차체형태 (세단/SUV/해치백 등)
|
||||
door_count = Column(Integer) # 도어수
|
||||
seating_capacity = Column(Integer) # 승차정원
|
||||
|
||||
# 제원
|
||||
length = Column(Integer) # 전장 (mm)
|
||||
width = Column(Integer) # 전폭 (mm)
|
||||
height = Column(Integer) # 전고 (mm)
|
||||
wheelbase = Column(Integer) # 축거 (mm)
|
||||
curb_weight = Column(Integer) # 공차중량 (kg)
|
||||
|
||||
# 옵션/편의장치 (JSON 배열)
|
||||
safety_options = Column(JSON) # 안전옵션 ["에어백", "ABS", ...]
|
||||
comfort_options = Column(JSON) # 편의옵션 ["썬루프", "열선시트", ...]
|
||||
exterior_options = Column(JSON) # 외장옵션 ["LED헤드램프", ...]
|
||||
interior_options = Column(JSON) # 내장옵션 ["가죽시트", ...]
|
||||
|
||||
# 원본 데이터
|
||||
raw_data = Column(JSON) # 전체 원본 데이터 (파싱하지 못한 정보 포함)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
# Relationship
|
||||
car = relationship("Car", back_populates="specification")
|
||||
48
backend/app/models/cc_package.py
Normal file
48
backend/app/models/cc_package.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, Float
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class CCPackage(Base):
|
||||
"""CC charging packages"""
|
||||
__tablename__ = "cc_packages"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(50), nullable=False) # e.g., "Basic", "Standard", "Premium"
|
||||
price_usd = Column(Integer, nullable=False) # Price in USD (10, 27, 40)
|
||||
cc_amount = Column(Integer, nullable=False) # CC amount (10, 30, 50)
|
||||
bonus_cc = Column(Integer, default=0) # Bonus CC (0, 3, 10)
|
||||
discount_percent = Column(Integer, default=0) # Discount percentage (0, 10, 20)
|
||||
is_active = Column(Boolean, default=True)
|
||||
sort_order = Column(Integer, default=0)
|
||||
|
||||
# Stripe Price ID for recurring or one-time payments
|
||||
stripe_price_id = Column(String(100), nullable=True)
|
||||
|
||||
|
||||
# Default CC packages
|
||||
DEFAULT_CC_PACKAGES = [
|
||||
{
|
||||
"name": "Basic",
|
||||
"price_usd": 10,
|
||||
"cc_amount": 10,
|
||||
"bonus_cc": 0,
|
||||
"discount_percent": 0,
|
||||
"sort_order": 1,
|
||||
},
|
||||
{
|
||||
"name": "Standard",
|
||||
"price_usd": 27,
|
||||
"cc_amount": 27,
|
||||
"bonus_cc": 3,
|
||||
"discount_percent": 10,
|
||||
"sort_order": 2,
|
||||
},
|
||||
{
|
||||
"name": "Premium",
|
||||
"price_usd": 40,
|
||||
"cc_amount": 40,
|
||||
"bonus_cc": 10,
|
||||
"discount_percent": 20,
|
||||
"sort_order": 3,
|
||||
},
|
||||
]
|
||||
85
backend/app/models/dealer.py
Normal file
85
backend/app/models/dealer.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Float
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
import uuid
|
||||
import hashlib
|
||||
from ..database import Base
|
||||
|
||||
|
||||
def generate_dealer_code():
|
||||
"""Generate a unique 6-character dealer code"""
|
||||
unique_id = uuid.uuid4().hex
|
||||
return "D" + hashlib.sha256(unique_id.encode()).hexdigest()[:5].upper()
|
||||
|
||||
|
||||
class DealerApplication(Base):
|
||||
"""Dealer application for users wanting to become dealers"""
|
||||
__tablename__ = "dealer_applications"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
|
||||
# Business info
|
||||
business_name = Column(String(100), nullable=False) # 상호명
|
||||
business_number = Column(String(50), nullable=True) # 사업자번호 (선택)
|
||||
|
||||
# Personal info
|
||||
real_name = Column(String(100), nullable=False) # 실명
|
||||
id_number_encrypted = Column(String(255), nullable=True) # 주민번호/외국인번호 (암호화)
|
||||
phone = Column(String(50), nullable=False) # 연락처
|
||||
|
||||
# Bank info for withdrawals
|
||||
bank_name = Column(String(50), nullable=False) # 은행명
|
||||
bank_account = Column(String(100), nullable=False) # 계좌번호
|
||||
account_holder = Column(String(100), nullable=False) # 예금주명
|
||||
|
||||
# Photo
|
||||
photo_url = Column(String(500), nullable=True) # 본인 사진 URL
|
||||
|
||||
# Application status
|
||||
status = Column(String(20), default="pending") # pending, approved, rejected
|
||||
rejected_reason = Column(Text, nullable=True) # 거부 사유
|
||||
|
||||
# Timestamps
|
||||
applied_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
approved_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="dealer_application")
|
||||
|
||||
|
||||
class DealerInfo(Base):
|
||||
"""Approved dealer information"""
|
||||
__tablename__ = "dealer_info"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), unique=True, nullable=False)
|
||||
|
||||
# Dealer identification
|
||||
dealer_code = Column(String(10), unique=True, index=True, nullable=False) # 딜러 고유 코드 (D + 5자리)
|
||||
dealer_card_url = Column(String(500), nullable=True) # 딜러증 이미지 URL
|
||||
|
||||
# Business info (from application)
|
||||
business_name = Column(String(100), nullable=False)
|
||||
real_name = Column(String(100), nullable=False)
|
||||
phone = Column(String(50), nullable=False)
|
||||
photo_url = Column(String(500), nullable=True)
|
||||
|
||||
# Bank info (from application)
|
||||
bank_name = Column(String(50), nullable=False)
|
||||
bank_account = Column(String(100), nullable=False)
|
||||
account_holder = Column(String(100), nullable=False)
|
||||
|
||||
# Earnings
|
||||
total_commission_earned = Column(Float, default=0.0) # 총 수수료 수익 (KRW)
|
||||
total_withdrawn = Column(Float, default=0.0) # 총 출금액 (KRW)
|
||||
pending_withdrawal = Column(Float, default=0.0) # 출금 대기 금액 (KRW)
|
||||
|
||||
# Status
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="dealer_info")
|
||||
46
backend/app/models/exchange_rate.py
Normal file
46
backend/app/models/exchange_rate.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
Exchange Rate Model - 환율 정보 저장
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean
|
||||
from sqlalchemy.sql import func
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class ExchangeRate(Base):
|
||||
"""환율 정보 테이블"""
|
||||
__tablename__ = "exchange_rates"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
# 통화 정보
|
||||
currency_code = Column(String(10), unique=True, index=True) # USD, MNT, RUB, CNY
|
||||
currency_name = Column(String(100)) # 미국 달러, 몽골 투그릭 등
|
||||
|
||||
# 환율 정보 (한국수출입은행 기준)
|
||||
deal_base_rate = Column(Float) # 매매기준율 (1 USD = X KRW)
|
||||
ttb_rate = Column(Float) # 전신환(송금) 받을때
|
||||
tts_rate = Column(Float) # 전신환(송금) 보낼때
|
||||
|
||||
# 가중치 적용 환율
|
||||
weight_percent = Column(Float, default=0.0) # 관리자 설정 가중치 (%)
|
||||
adjusted_rate = Column(Float) # 가중치 적용된 환율
|
||||
|
||||
# 메타 정보
|
||||
source_date = Column(String(20)) # 수출입은행 기준일 (예: 20241223)
|
||||
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())
|
||||
|
||||
|
||||
class ExchangeRateHistory(Base):
|
||||
"""환율 변동 이력 테이블"""
|
||||
__tablename__ = "exchange_rate_history"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
currency_code = Column(String(10), index=True)
|
||||
deal_base_rate = Column(Float)
|
||||
source_date = Column(String(20))
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
67
backend/app/models/hero_banner.py
Normal file
67
backend/app/models/hero_banner.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class HeroBannerSettings(Base):
|
||||
"""히어로 배너 슬라이더 설정"""
|
||||
__tablename__ = "hero_banner_settings"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
# 슬라이드 전환 간격 (밀리초)
|
||||
slide_interval = Column(Integer, default=3000) # 3초
|
||||
|
||||
# 애니메이션 타입: 'film-strip', 'fade', 'slide'
|
||||
animation_type = Column(String(20), default="film-strip")
|
||||
|
||||
# 이미지 크기
|
||||
image_width = Column(Integer, default=500)
|
||||
image_height = Column(Integer, default=300)
|
||||
|
||||
# 자동 재생 여부
|
||||
auto_play = 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())
|
||||
|
||||
|
||||
class HeroBanner(Base):
|
||||
"""히어로 배너 이미지"""
|
||||
__tablename__ = "hero_banners"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
# 다국어 제목
|
||||
title_ko = Column(String(100))
|
||||
title_en = Column(String(100))
|
||||
title_mn = Column(String(100)) # 몽골어
|
||||
|
||||
# 다국어 서브타이틀
|
||||
subtitle_ko = Column(String(200))
|
||||
subtitle_en = Column(String(200))
|
||||
subtitle_mn = Column(String(200))
|
||||
|
||||
# 이미지 URL
|
||||
image_url = Column(String(500), nullable=False)
|
||||
|
||||
# 클릭 시 이동 URL (선택)
|
||||
link_url = Column(String(500))
|
||||
|
||||
# 연결된 차량 ID (선택 - 차량 상세 페이지로 연결)
|
||||
car_id = Column(Integer, ForeignKey("cars.id", ondelete="SET NULL"), nullable=True)
|
||||
|
||||
# 활성화 여부
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
# 표시 순서 (낮을수록 먼저)
|
||||
display_order = Column(Integer, default=0)
|
||||
|
||||
# 타임스탬프
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
# 관계
|
||||
car = relationship("Car", foreign_keys=[car_id])
|
||||
79
backend/app/models/inquiry.py
Normal file
79
backend/app/models/inquiry.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, ForeignKey
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship
|
||||
import enum
|
||||
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class InquiryStatus:
|
||||
PENDING = "pending"
|
||||
IN_PROGRESS = "in_progress"
|
||||
RESOLVED = "resolved"
|
||||
CLOSED = "closed"
|
||||
|
||||
|
||||
class InquiryCategory:
|
||||
GENERAL = "general"
|
||||
VEHICLE = "vehicle"
|
||||
PAYMENT = "payment"
|
||||
SHIPPING = "shipping"
|
||||
DEALER = "dealer"
|
||||
ACCOUNT = "account"
|
||||
OTHER = "other"
|
||||
|
||||
|
||||
class Inquiry(Base):
|
||||
"""User inquiry/support ticket"""
|
||||
__tablename__ = "inquiries"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
|
||||
# Legacy field for backward compatibility
|
||||
car_id = Column(Integer, ForeignKey("cars.id"), nullable=True)
|
||||
|
||||
# Inquiry details
|
||||
category = Column(String(50), default=InquiryCategory.GENERAL)
|
||||
subject = Column(String(200), nullable=True)
|
||||
message = Column(Text, nullable=False)
|
||||
|
||||
# Contact info (can be different from user's profile)
|
||||
contact_email = Column(String(255), nullable=True)
|
||||
contact_phone = Column(String(50), nullable=True)
|
||||
|
||||
# Status
|
||||
status = Column(String(20), default=InquiryStatus.PENDING)
|
||||
|
||||
# Admin response
|
||||
admin_response = Column(Text, nullable=True)
|
||||
responded_at = Column(DateTime(timezone=True), nullable=True)
|
||||
responded_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", foreign_keys=[user_id], back_populates="inquiries")
|
||||
responder = relationship("User", foreign_keys=[responded_by])
|
||||
# car relationship disabled due to schema mismatch - Car model doesn't have inquiries relationship
|
||||
# car = relationship("Car", back_populates="inquiries")
|
||||
|
||||
|
||||
class InquiryMessage(Base):
|
||||
"""Messages within an inquiry thread"""
|
||||
__tablename__ = "inquiry_messages"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
inquiry_id = Column(Integer, ForeignKey("inquiries.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
|
||||
message = Column(Text, nullable=False)
|
||||
is_admin = Column(Boolean, default=False)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
inquiry = relationship("Inquiry", backref="messages")
|
||||
user = relationship("User")
|
||||
37
backend/app/models/notification.py
Normal file
37
backend/app/models/notification.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class Notification(Base):
|
||||
"""User notifications"""
|
||||
__tablename__ = "notifications"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
|
||||
# Notification type: vehicle_recommended, shipping_update, withdrawal_processed,
|
||||
# referral_reward, dealer_approved, share_purchased, system
|
||||
notification_type = Column(String(50), nullable=False)
|
||||
|
||||
# Title and message (supports i18n keys or direct text)
|
||||
title = Column(String(200), nullable=False)
|
||||
message = Column(Text, nullable=False)
|
||||
|
||||
# Optional link to navigate when clicked
|
||||
link = Column(String(500), nullable=True)
|
||||
|
||||
# Related entity (optional)
|
||||
related_id = Column(Integer, nullable=True) # ID of related entity
|
||||
related_type = Column(String(50), nullable=True) # Type: vehicle_request, purchased_vehicle, withdrawal, etc.
|
||||
|
||||
# Status
|
||||
is_read = Column(Boolean, default=False)
|
||||
read_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", backref="notifications")
|
||||
119
backend/app/models/performance_check.py
Normal file
119
backend/app/models/performance_check.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
성능점검표 (Performance Check Report) 모델
|
||||
카모두에서 가져온 차량 성능점검 정보를 저장
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, DateTime, Text, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class CarPerformanceCheck(Base):
|
||||
"""차량 성능점검표"""
|
||||
__tablename__ = "car_performance_checks"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
car_id = Column(Integer, ForeignKey("cars.id", ondelete="CASCADE"), nullable=False, unique=True)
|
||||
|
||||
# 성능점검 기본정보
|
||||
check_number = Column(String(50)) # 성능점검번호
|
||||
check_date = Column(String(20)) # 점검일자
|
||||
valid_until = Column(String(20)) # 유효기간
|
||||
inspector_name = Column(String(50)) # 점검자명
|
||||
inspector_license = Column(String(50)) # 점검자 자격번호
|
||||
|
||||
# 차량 기본정보 (car_number는 cars 테이블에서 관리 - 원자성)
|
||||
first_registration = Column(String(20)) # 최초등록일
|
||||
model_year = Column(String(20)) # 연식
|
||||
|
||||
# 주행거리
|
||||
mileage = Column(Integer) # 주행거리
|
||||
mileage_status = Column(String(20)) # 주행거리 상태 (정상/조작의심/교환됨)
|
||||
|
||||
# 압류/저당 정보
|
||||
seize_count = Column(Integer, default=0) # 압류 건수
|
||||
collateral_count = Column(Integer, default=0) # 저당 건수
|
||||
|
||||
# 특별 이력 (침수/화재/전손)
|
||||
is_flood_damaged = Column(Boolean, default=False) # 침수
|
||||
is_fire_damaged = Column(Boolean, default=False) # 화재
|
||||
is_total_loss = Column(Boolean, default=False) # 전손
|
||||
|
||||
# 용도이력
|
||||
usage_history = Column(String(100)) # 자가용/영업용/관용 등
|
||||
is_rental_used = Column(Boolean, default=False) # 렌트 이력
|
||||
|
||||
# 주요장치 상태 (JSON으로 상세정보 저장)
|
||||
# 각 항목: 양호/주의/불량
|
||||
engine_status = Column(String(20)) # 원동기
|
||||
transmission_status = Column(String(20)) # 변속기
|
||||
power_delivery_status = Column(String(20)) # 동력전달
|
||||
steering_status = Column(String(20)) # 조향장치
|
||||
brake_status = Column(String(20)) # 제동장치
|
||||
electrical_status = Column(String(20)) # 전기장치
|
||||
fuel_system_status = Column(String(20)) # 연료장치
|
||||
|
||||
# 타이어 상태
|
||||
tire_front_left = Column(String(20)) # 전좌
|
||||
tire_front_right = Column(String(20)) # 전우
|
||||
tire_rear_left = Column(String(20)) # 후좌
|
||||
tire_rear_right = Column(String(20)) # 후우
|
||||
|
||||
# 사고 이력 (외판/주요골격) - JSON으로 상세 저장
|
||||
# 부위별: 없음/교환/판금용접/부식/손상
|
||||
accident_history = Column(JSON) # {"hood": "교환", "front_fender_left": "판금", ...}
|
||||
|
||||
# 외판 부위
|
||||
hood = Column(String(20)) # 후드
|
||||
front_fender_left = Column(String(20)) # 프론트휀더(좌)
|
||||
front_fender_right = Column(String(20)) # 프론트휀더(우)
|
||||
front_door_left = Column(String(20)) # 프론트도어(좌)
|
||||
front_door_right = Column(String(20)) # 프론트도어(우)
|
||||
rear_door_left = Column(String(20)) # 리어도어(좌)
|
||||
rear_door_right = Column(String(20)) # 리어도어(우)
|
||||
trunk_lid = Column(String(20)) # 트렁크리드
|
||||
radiator_support = Column(String(20)) # 라디에이터서포트
|
||||
roof_panel = Column(String(20)) # 루프패널
|
||||
quarter_panel_left = Column(String(20)) # 쿼터패널(좌)
|
||||
quarter_panel_right = Column(String(20)) # 쿼터패널(우)
|
||||
side_sill_left = Column(String(20)) # 사이드실패널(좌)
|
||||
side_sill_right = Column(String(20)) # 사이드실패널(우)
|
||||
|
||||
# 주요골격 부위
|
||||
front_panel = Column(String(20)) # 프론트패널
|
||||
cross_member = Column(String(20)) # 크로스멤버
|
||||
inside_panel_left = Column(String(20)) # 인사이드패널(좌)
|
||||
inside_panel_right = Column(String(20)) # 인사이드패널(우)
|
||||
side_member_left = Column(String(20)) # 사이드멤버(좌)
|
||||
side_member_right = Column(String(20)) # 사이드멤버(우)
|
||||
wheel_house_left = Column(String(20)) # 휠하우스(좌)
|
||||
wheel_house_right = Column(String(20)) # 휠하우스(우)
|
||||
dash_panel = Column(String(20)) # 대쉬패널
|
||||
floor_panel = Column(String(20)) # 플로어패널
|
||||
trunk_floor = Column(String(20)) # 트렁크플로어
|
||||
rear_panel = Column(String(20)) # 리어패널
|
||||
pillar_a_left = Column(String(20)) # 필러A(좌)
|
||||
pillar_a_right = Column(String(20)) # 필러A(우)
|
||||
pillar_b_left = Column(String(20)) # 필러B(좌)
|
||||
pillar_b_right = Column(String(20)) # 필러B(우)
|
||||
pillar_c_left = Column(String(20)) # 필러C(좌)
|
||||
pillar_c_right = Column(String(20)) # 필러C(우)
|
||||
package_tray = Column(String(20)) # 패키지트레이
|
||||
|
||||
# 원본 데이터 (파싱하지 못한 추가 정보)
|
||||
raw_data = Column(JSON) # 전체 원본 데이터
|
||||
raw_html = Column(Text) # 원본 HTML (디버깅용)
|
||||
|
||||
# 점검표 이미지 URL
|
||||
report_image_url = Column(String(500)) # 성능점검표 이미지
|
||||
report_image_local = Column(String(500)) # 로컬 저장 경로
|
||||
|
||||
# PDF 파일 경로 (Playwright로 캡처한 성능점검표)
|
||||
pdf_path = Column(String(500)) # PDF 파일 상대경로 (/uploads/performance_checks/xxx.pdf)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
# Relationship
|
||||
car = relationship("Car", back_populates="performance_check")
|
||||
48
backend/app/models/push_subscription.py
Normal file
48
backend/app/models/push_subscription.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class PushSubscription(Base):
|
||||
"""Store user's push notification subscriptions"""
|
||||
__tablename__ = "push_subscriptions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
endpoint = Column(Text, nullable=False) # Push service endpoint URL
|
||||
p256dh_key = Column(String(255), nullable=False) # Public key for encryption
|
||||
auth_key = Column(String(255), nullable=False) # Auth secret for encryption
|
||||
device_info = Column(String(255), nullable=True) # Browser/device info
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
last_used_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
user = relationship("User", backref="push_subscriptions")
|
||||
|
||||
|
||||
class UserNotificationPreference(Base):
|
||||
"""User preferences for different notification types"""
|
||||
__tablename__ = "user_notification_preferences"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, unique=True)
|
||||
|
||||
# Notification type preferences (True = enabled)
|
||||
vehicle_recommended = Column(Boolean, default=True)
|
||||
shipping_update = Column(Boolean, default=True)
|
||||
payment_confirmed = Column(Boolean, default=True)
|
||||
withdrawal_processed = Column(Boolean, default=True)
|
||||
dealer_status = Column(Boolean, default=True)
|
||||
share_purchased = Column(Boolean, default=True)
|
||||
referral_reward = Column(Boolean, default=True)
|
||||
inquiry_reply = Column(Boolean, default=True)
|
||||
system_announcements = Column(Boolean, default=True)
|
||||
|
||||
# Channel preferences
|
||||
push_enabled = Column(Boolean, default=True)
|
||||
email_enabled = Column(Boolean, default=False)
|
||||
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
user = relationship("User", backref="notification_preferences")
|
||||
37
backend/app/models/referral.py
Normal file
37
backend/app/models/referral.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class ReferralReward(Base):
|
||||
"""레퍼럴 보상 모델"""
|
||||
__tablename__ = "referral_rewards"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
# 추천인 (보상 받는 사람)
|
||||
referrer_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
|
||||
# 피추천인 (추천받아 가입한 사람)
|
||||
referred_user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
|
||||
# 결제 금액 (피추천인이 충전한 금액 USD)
|
||||
payment_amount = Column(Float, nullable=False)
|
||||
|
||||
# 보상 금액 (결제 금액의 X%)
|
||||
reward_amount = Column(Float, nullable=False)
|
||||
|
||||
# 보상 상태: pending(대기), credited(적립), withdrawn(출금)
|
||||
status = Column(String(20), default="pending")
|
||||
|
||||
# 출금 요청 ID (출금 시 연결)
|
||||
withdrawal_request_id = Column(Integer, ForeignKey("withdrawal_requests.id"), nullable=True)
|
||||
|
||||
# 타임스탬프
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
credited_at = Column(DateTime(timezone=True), nullable=True) # 적립 시각
|
||||
|
||||
# Relationships
|
||||
referrer = relationship("User", foreign_keys=[referrer_id], backref="referral_rewards_given")
|
||||
referred_user = relationship("User", foreign_keys=[referred_user_id], backref="referral_rewards_received")
|
||||
45
backend/app/models/settings.py
Normal file
45
backend/app/models/settings.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean
|
||||
from sqlalchemy.sql import func
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class SystemSettings(Base):
|
||||
"""시스템 설정"""
|
||||
__tablename__ = "system_settings"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
# 검색 결과 페이지 크기
|
||||
search_page_size = Column(Integer, default=20)
|
||||
|
||||
# 마진 설정 (%)
|
||||
korea_margin_percent = Column(Float, default=5.0)
|
||||
mongolia_margin_percent = Column(Float, default=5.0)
|
||||
|
||||
# CC 코인 설정
|
||||
cc_per_usdc = Column(Integer, default=10) # 1 USDC = 10 CC
|
||||
cc_per_view = Column(Integer, default=1) # 차량 상세 조회 시 1 CC
|
||||
cc_signup_bonus = Column(Integer, default=3) # 신규 가입 시 3 CC
|
||||
cars_per_cc = Column(Integer, default=3) # 1 CC당 추천 차량 수 (기본 3대)
|
||||
|
||||
# 캐시 TTL (시간)
|
||||
cache_ttl_hours = Column(Integer, default=2)
|
||||
|
||||
# 컨테이너 물류비 설정 (USD)
|
||||
container_logistics_usd = Column(Integer, default=3600) # 컨테이너 물류비 $3,600
|
||||
shoring_cost_usd = Column(Integer, default=300) # 쇼링비 (컨테이너 고정) $300
|
||||
|
||||
# 레퍼럴 보상 설정
|
||||
referral_reward_enabled = Column(Boolean, default=True) # 레퍼럴 보상 활성화
|
||||
referral_reward_percent = Column(Float, default=10.0) # 보상 비율 (기본 10%)
|
||||
referral_reward_type = Column(String(20), default="one_time") # one_time / recurring
|
||||
|
||||
# 환율 가중치 설정 (%)
|
||||
exchange_rate_weight_usd = Column(Float, default=0.0) # USD 가중치
|
||||
exchange_rate_weight_mnt = Column(Float, default=0.0) # MNT (몽골 투그릭) 가중치
|
||||
exchange_rate_weight_rub = Column(Float, default=0.0) # RUB (러시아 루블) 가중치
|
||||
exchange_rate_weight_cny = Column(Float, default=0.0) # CNY (중국 위안) 가중치
|
||||
|
||||
# 타임스탬프
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
28
backend/app/models/translation.py
Normal file
28
backend/app/models/translation.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Index
|
||||
from sqlalchemy.sql import func
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class Translation(Base):
|
||||
"""Translation dictionary for car-related terms"""
|
||||
__tablename__ = "translations"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
# Source text (Korean)
|
||||
source_text = Column(String(500), nullable=False, index=True)
|
||||
|
||||
# Category: maker, model, fuel, transmission, color, car_name, etc.
|
||||
category = Column(String(50), nullable=False, index=True)
|
||||
|
||||
# Translations
|
||||
text_en = Column(String(500)) # English
|
||||
text_mn = Column(String(500)) # Mongolian
|
||||
text_ru = Column(String(500)) # Russian
|
||||
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
|
||||
__table_args__ = (
|
||||
Index('ix_translations_source_category', 'source_text', 'category', unique=True),
|
||||
)
|
||||
138
backend/app/models/user.py
Normal file
138
backend/app/models/user.py
Normal file
@@ -0,0 +1,138 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Float
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
import uuid
|
||||
import hashlib
|
||||
from ..database import Base
|
||||
|
||||
|
||||
def generate_referral_code():
|
||||
"""Generate a unique 8-character referral code"""
|
||||
unique_id = uuid.uuid4().hex
|
||||
return hashlib.sha256(unique_id.encode()).hexdigest()[:8].upper()
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
email = Column(String(255), unique=True, nullable=False, index=True)
|
||||
password_hash = Column(String(255), nullable=False)
|
||||
name = Column(String(100))
|
||||
phone = Column(String(50))
|
||||
country = Column(String(50), default="Mongolia")
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_admin = Column(Boolean, default=False)
|
||||
is_dealer = Column(Boolean, default=False) # Dealer status
|
||||
cc_balance = Column(Float, default=3.0) # CC coin balance, 3 free on signup
|
||||
referral_code = Column(String(8), unique=True, index=True) # Unique referral code for sharing
|
||||
referred_by = Column(String(8), nullable=True) # Referral code of the user who referred this user
|
||||
|
||||
# Email verification
|
||||
email_verified = Column(Boolean, default=False)
|
||||
email_verified_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Phone verification
|
||||
phone_verified = Column(Boolean, default=False)
|
||||
phone_verified_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Account withdrawal/deletion
|
||||
withdrawal_requested_at = Column(DateTime(timezone=True), nullable=True) # 탈퇴 요청 시각
|
||||
withdrawal_reason = Column(String(500), nullable=True) # 탈퇴 사유
|
||||
deleted_at = Column(DateTime(timezone=True), nullable=True) # 실제 삭제 시각 (soft delete)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Note: foreign_keys specified as string to avoid circular import
|
||||
inquiries = relationship("Inquiry", back_populates="user", primaryjoin="User.id == Inquiry.user_id")
|
||||
car_views = relationship("CarView", back_populates="user")
|
||||
performance_check_views = relationship("PerformanceCheckView", back_populates="user")
|
||||
charge_history = relationship("ChargeHistory", back_populates="user", primaryjoin="User.id == ChargeHistory.user_id")
|
||||
dealer_application = relationship("DealerApplication", back_populates="user", uselist=False)
|
||||
dealer_info = relationship("DealerInfo", back_populates="user", uselist=False)
|
||||
|
||||
|
||||
class VerificationCode(Base):
|
||||
"""Store temporary verification codes for email and phone"""
|
||||
__tablename__ = "verification_codes"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=True) # Nullable for pre-registration
|
||||
email = Column(String(255), nullable=True, index=True) # For email verification
|
||||
phone = Column(String(50), nullable=True, index=True) # For phone verification
|
||||
code = Column(String(10), nullable=False) # 6-digit code
|
||||
code_type = Column(String(20), nullable=False) # 'email' or 'phone'
|
||||
purpose = Column(String(50), default="verification") # 'verification', 'password_reset'
|
||||
attempts = Column(Integer, default=0) # Failed verification attempts
|
||||
max_attempts = Column(Integer, default=5)
|
||||
expires_at = Column(DateTime(timezone=True), nullable=False)
|
||||
verified_at = Column(DateTime(timezone=True), nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
|
||||
class CarView(Base):
|
||||
"""Track which cars a user has purchased (paid CC to view full details)"""
|
||||
__tablename__ = "car_views"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
car_id = Column(Integer, ForeignKey("cars.id"), nullable=False)
|
||||
cc_paid = Column(Integer, default=1) # CC paid for this view
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
user = relationship("User", back_populates="car_views")
|
||||
car = relationship("Car", back_populates="views")
|
||||
|
||||
|
||||
class PerformanceCheckView(Base):
|
||||
"""Track which performance checks a user has purchased (paid 0.1 CC to view)"""
|
||||
__tablename__ = "performance_check_views"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
car_id = Column(Integer, ForeignKey("cars.id"), nullable=False)
|
||||
cc_paid = Column(Float, default=0.1) # CC paid for this view (0.1 CC)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
user = relationship("User", back_populates="performance_check_views")
|
||||
|
||||
|
||||
class ChargeHistory(Base):
|
||||
"""Track CC charge history for users"""
|
||||
__tablename__ = "charge_history"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
package_id = Column(Integer, ForeignKey("cc_packages.id"), nullable=True) # CC package purchased
|
||||
amount = Column(Integer, nullable=False) # Amount in selected currency
|
||||
amount_usd = Column(Integer, nullable=True) # Amount in USD (for backwards compatibility)
|
||||
cc_amount = Column(Integer, nullable=False) # CC received
|
||||
bonus_cc = Column(Integer, default=0) # Bonus CC received
|
||||
currency = Column(String(10), default="USD") # USD, USDC, KRW
|
||||
payment_method = Column(String(50), default="stripe") # stripe, manual, usdc, bank_transfer
|
||||
|
||||
# Stripe fields
|
||||
stripe_session_id = Column(String(200), nullable=True) # Stripe Checkout Session ID
|
||||
stripe_payment_intent_id = Column(String(200), nullable=True) # Stripe Payment Intent ID
|
||||
|
||||
# Legacy fields
|
||||
transaction_id = Column(String(100), nullable=True) # External transaction ID (crypto tx hash)
|
||||
wallet_address = Column(String(100), nullable=True) # User's wallet address for refunds
|
||||
|
||||
admin_note = Column(String(500), nullable=True) # Admin notes
|
||||
status = Column(String(20), default="pending") # pending, completed, failed, cancelled
|
||||
verified_at = Column(DateTime(timezone=True), nullable=True)
|
||||
verified_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
user = relationship("User", back_populates="charge_history", foreign_keys=[user_id])
|
||||
|
||||
|
||||
# Payment settings constants
|
||||
class PaymentSettings:
|
||||
USDC_WALLET_ADDRESS = "0x1234567890abcdef1234567890abcdef12345678" # Platform USDC receiving address
|
||||
USDC_NETWORK = "Polygon" # Default network (Polygon for low fees)
|
||||
MIN_CHARGE_USD = 10
|
||||
MAX_CHARGE_USD = 10000
|
||||
SUPPORTED_CURRENCIES = ["USD", "USDC", "KRW"]
|
||||
SUPPORTED_METHODS = ["card", "usdc", "bank_transfer"]
|
||||
106
backend/app/models/vehicle_request.py
Normal file
106
backend/app/models/vehicle_request.py
Normal file
@@ -0,0 +1,106 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, JSON, Float
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class VehicleRequest(Base):
|
||||
"""Track vehicle search requests from users"""
|
||||
__tablename__ = "vehicle_requests"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
|
||||
# Search criteria
|
||||
maker_code = Column(String(50))
|
||||
maker_name = Column(String(100))
|
||||
model_code = Column(String(50))
|
||||
model_name = Column(String(100))
|
||||
grade_code = Column(String(50))
|
||||
grade_name = Column(String(100))
|
||||
year_from = Column(Integer)
|
||||
year_to = Column(Integer)
|
||||
mileage_min = Column(Integer)
|
||||
mileage_max = Column(Integer)
|
||||
fuel = Column(String(50)) # 연료 타입 (휘발유, 경유, 하이브리드, LPG, 전기)
|
||||
displacement_min = Column(Integer) # 최소 배기량 (cc)
|
||||
displacement_max = Column(Integer) # 최대 배기량 (cc)
|
||||
|
||||
# CC payment for request submission
|
||||
cc_paid = Column(Float, default=1.0) # CC paid for this request (1 CC)
|
||||
|
||||
# Status: pending, reviewed, completed
|
||||
status = Column(String(20), default="pending")
|
||||
admin_reviewed_at = Column(DateTime(timezone=True))
|
||||
admin_notes = Column(Text)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", backref="vehicle_requests")
|
||||
recommended_vehicles = relationship("RequestVehicle", back_populates="request", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class RequestVehicle(Base):
|
||||
"""Vehicles recommended by admin for a user's request"""
|
||||
__tablename__ = "request_vehicles"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
request_id = Column(Integer, ForeignKey("vehicle_requests.id"), nullable=False)
|
||||
|
||||
# Car data from Carmodoo (stored as JSON)
|
||||
car_data = Column(JSON, nullable=False)
|
||||
|
||||
# Admin approval
|
||||
is_approved = Column(Boolean, default=False)
|
||||
approved_at = Column(DateTime(timezone=True))
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
request = relationship("VehicleRequest", back_populates="recommended_vehicles")
|
||||
|
||||
|
||||
class PurchasedVehicle(Base):
|
||||
"""Track purchased vehicles and their shipping status"""
|
||||
__tablename__ = "purchased_vehicles"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
|
||||
# Vehicle info
|
||||
car_name = Column(String(200))
|
||||
car_data = Column(JSON) # Full car details
|
||||
car_image = Column(String(500)) # Main image URL
|
||||
|
||||
# Price info
|
||||
vehicle_price_krw = Column(Integer)
|
||||
domestic_cost_krw = Column(Integer)
|
||||
shipping_cost_usd = Column(Integer)
|
||||
total_cost_krw = Column(Integer)
|
||||
car_type = Column(String(20)) # small, compact
|
||||
|
||||
# Dealer selection and commission (50/50 split of Mongolia margin)
|
||||
selected_dealer_id = Column(Integer, ForeignKey("dealer_info.id"), nullable=True)
|
||||
dealer_commission_krw = Column(Integer, default=0) # 50% of Mongolia margin
|
||||
platform_commission_krw = Column(Integer, default=0) # 50% of Mongolia margin
|
||||
commission_paid = Column(Boolean, default=False) # Whether commission has been paid
|
||||
commission_paid_at = Column(DateTime(timezone=True))
|
||||
|
||||
# Shipping status: 1-5
|
||||
# 1: Purchased, 2: Incheon Port, 3: In Transit, 4: Customs, 5: Delivered
|
||||
shipping_status = Column(Integer, default=1)
|
||||
status_updated_at = Column(DateTime(timezone=True))
|
||||
|
||||
# Location info
|
||||
current_location = Column(String(200))
|
||||
estimated_arrival = Column(DateTime(timezone=True))
|
||||
|
||||
# Timestamps
|
||||
purchased_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
delivered_at = Column(DateTime(timezone=True))
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", backref="purchased_vehicles")
|
||||
selected_dealer = relationship("DealerInfo", backref="purchased_vehicles")
|
||||
75
backend/app/models/vehicle_share.py
Normal file
75
backend/app/models/vehicle_share.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Float, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
import uuid
|
||||
import hashlib
|
||||
from ..database import Base
|
||||
|
||||
|
||||
def generate_share_code():
|
||||
"""Generate a unique 10-character share code"""
|
||||
unique_id = uuid.uuid4().hex
|
||||
return hashlib.sha256(unique_id.encode()).hexdigest()[:10].upper()
|
||||
|
||||
|
||||
class VehicleShare(Base):
|
||||
"""Track vehicle shares with price markup"""
|
||||
__tablename__ = "vehicle_shares"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False) # User who shared
|
||||
|
||||
# Reference to the original vehicle
|
||||
request_vehicle_id = Column(Integer, ForeignKey("request_vehicles.id"), nullable=False)
|
||||
|
||||
# Share code for the link
|
||||
share_code = Column(String(10), unique=True, index=True, nullable=False)
|
||||
|
||||
# Pricing
|
||||
original_price_krw = Column(Float, nullable=False) # Original vehicle price
|
||||
markup_amount_krw = Column(Float, default=0) # Additional amount added by sharer
|
||||
shared_price_krw = Column(Float, nullable=False) # Total shared price (original + markup)
|
||||
|
||||
# Statistics
|
||||
view_count = Column(Integer, default=0)
|
||||
is_purchased = Column(Boolean, default=False)
|
||||
purchased_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
expires_at = Column(DateTime(timezone=True), nullable=True) # Optional expiration
|
||||
purchased_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", foreign_keys=[user_id], backref="vehicle_shares")
|
||||
purchased_by = relationship("User", foreign_keys=[purchased_by_user_id])
|
||||
request_vehicle = relationship("RequestVehicle", backref="shares")
|
||||
|
||||
|
||||
class ShareReward(Base):
|
||||
"""Track rewards earned from vehicle shares"""
|
||||
__tablename__ = "share_rewards"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False) # User who earned the reward
|
||||
vehicle_share_id = Column(Integer, ForeignKey("vehicle_shares.id"), nullable=False)
|
||||
|
||||
# Amounts
|
||||
markup_amount = Column(Float, nullable=False) # Original markup amount
|
||||
reward_amount = Column(Float, nullable=False) # 90% of markup
|
||||
tax_amount = Column(Float, nullable=False) # 3.3% tax withholding
|
||||
net_amount = Column(Float, nullable=False) # Final amount after tax
|
||||
|
||||
# Status
|
||||
status = Column(String(20), default="pending") # pending, approved, withdrawn
|
||||
|
||||
# Withdrawal tracking
|
||||
withdrawal_request_id = Column(Integer, ForeignKey("withdrawal_requests.id"), nullable=True)
|
||||
withdrawn_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", backref="share_rewards")
|
||||
vehicle_share = relationship("VehicleShare", backref="reward")
|
||||
111
backend/app/models/visitor.py
Normal file
111
backend/app/models/visitor.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
Visitor tracking models for analytics
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Text, Index
|
||||
from sqlalchemy.sql import func
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class VisitorLog(Base):
|
||||
"""
|
||||
Raw visitor log - tracks every page visit
|
||||
IP addresses are hashed for privacy
|
||||
"""
|
||||
__tablename__ = "visitor_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
# Visitor identification (hashed for privacy)
|
||||
visitor_hash = Column(String(64), nullable=False, index=True) # SHA256 hash of IP + User-Agent
|
||||
ip_hash = Column(String(64), nullable=False) # SHA256 hash of IP only
|
||||
|
||||
# Session tracking
|
||||
session_id = Column(String(64), nullable=True, index=True) # Cookie-based session ID
|
||||
user_id = Column(Integer, nullable=True, index=True) # If logged in
|
||||
|
||||
# Page information
|
||||
page_path = Column(String(500), nullable=False, index=True)
|
||||
page_title = Column(String(200), nullable=True)
|
||||
referrer = Column(String(1000), nullable=True)
|
||||
referrer_domain = Column(String(200), nullable=True, index=True)
|
||||
|
||||
# Device information
|
||||
device_type = Column(String(20), nullable=True, index=True) # mobile, desktop, tablet
|
||||
browser = Column(String(50), nullable=True, index=True)
|
||||
browser_version = Column(String(20), nullable=True)
|
||||
os = Column(String(50), nullable=True)
|
||||
os_version = Column(String(20), nullable=True)
|
||||
|
||||
# Geographic information (from IP geolocation)
|
||||
country = Column(String(50), nullable=True, index=True)
|
||||
country_code = Column(String(5), nullable=True)
|
||||
city = Column(String(100), nullable=True)
|
||||
region = Column(String(100), nullable=True)
|
||||
|
||||
# UTM parameters
|
||||
utm_source = Column(String(100), nullable=True)
|
||||
utm_medium = Column(String(100), nullable=True)
|
||||
utm_campaign = Column(String(100), nullable=True)
|
||||
|
||||
# Timestamp
|
||||
visited_at = Column(DateTime(timezone=True), server_default=func.now(), index=True)
|
||||
|
||||
|
||||
class VisitorDailyStats(Base):
|
||||
"""
|
||||
Aggregated daily statistics for faster queries
|
||||
Pre-computed by a scheduled task
|
||||
"""
|
||||
__tablename__ = "visitor_daily_stats"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
stat_date = Column(String(10), nullable=False, unique=True, index=True) # YYYY-MM-DD
|
||||
|
||||
# Visitor counts
|
||||
total_visits = Column(Integer, default=0)
|
||||
unique_visitors = Column(Integer, default=0)
|
||||
|
||||
# Device breakdown (JSON string)
|
||||
device_breakdown = Column(Text) # {"mobile": 100, "desktop": 200, "tablet": 20}
|
||||
|
||||
# Browser breakdown (JSON string)
|
||||
browser_breakdown = Column(Text) # {"Chrome": 150, "Safari": 100, ...}
|
||||
|
||||
# Country breakdown (JSON string)
|
||||
country_breakdown = Column(Text) # {"MN": 200, "RU": 50, "KR": 30}
|
||||
|
||||
# Top pages (JSON string)
|
||||
top_pages = Column(Text) # [{"path": "/", "views": 500}, ...]
|
||||
|
||||
# Top referrers (JSON string)
|
||||
top_referrers = Column(Text) # [{"domain": "google.com", "visits": 100}, ...]
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
class VisitorSession(Base):
|
||||
"""
|
||||
Track visitor sessions for better analytics
|
||||
"""
|
||||
__tablename__ = "visitor_sessions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
session_id = Column(String(64), unique=True, nullable=False, index=True)
|
||||
visitor_hash = Column(String(64), nullable=False, index=True)
|
||||
user_id = Column(Integer, nullable=True)
|
||||
|
||||
# Session info
|
||||
first_page = Column(String(500))
|
||||
last_page = Column(String(500))
|
||||
page_count = Column(Integer, default=1)
|
||||
|
||||
# Device/geo info (copied from first visit)
|
||||
device_type = Column(String(20))
|
||||
browser = Column(String(50))
|
||||
country = Column(String(50))
|
||||
|
||||
# Timestamps
|
||||
started_at = Column(DateTime(timezone=True), server_default=func.now(), index=True)
|
||||
last_activity_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
35
backend/app/models/withdrawal.py
Normal file
35
backend/app/models/withdrawal.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Float, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from ..database import Base
|
||||
|
||||
|
||||
class WithdrawalRequest(Base):
|
||||
"""Track withdrawal requests from users"""
|
||||
__tablename__ = "withdrawal_requests"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
|
||||
# Amount details
|
||||
amount = Column(Float, nullable=False) # Requested withdrawal amount
|
||||
tax_withheld = Column(Float, default=0) # Tax amount withheld (3.3%)
|
||||
net_amount = Column(Float, nullable=False) # Net amount after tax
|
||||
|
||||
# Bank info (snapshot at time of request)
|
||||
bank_name = Column(String(50), nullable=False)
|
||||
bank_account = Column(String(100), nullable=False)
|
||||
account_holder = Column(String(100), nullable=False)
|
||||
|
||||
# Status
|
||||
status = Column(String(20), default="pending") # pending, approved, completed, rejected
|
||||
|
||||
# Admin notes
|
||||
admin_note = Column(Text, nullable=True)
|
||||
|
||||
# Timestamps
|
||||
requested_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
processed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", backref="withdrawal_requests")
|
||||
Reference in New Issue
Block a user