Files
AutonetSellCar/backend/app/api/sns_share.py
AutonetSellCar Deploy 7c943d8553 Add SNS Marketing Campaign feature
- Add cash_cc_balance to User model (withdrawable CC)
- Create SnsShareSubmission model for SNS share verification
- Add marketing campaign settings to SystemSettings
- Add reward_type to ReferralReward model
- Create /api/sns-share endpoints for submission and verification
- Add referral signup reward logic (10CC on signup)
- Create /sns-share user page for SNS sharing
- Create /admin/sns-shares management page
- Add marketing settings UI to admin settings page
- Add SNS Shares menu to admin sidebar

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 18:21:17 +09:00

278 lines
9.4 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from sqlalchemy import func
from typing import List, Optional
from datetime import datetime
from ..database import get_db
from ..models import User, Car, SnsShareSubmission, SystemSettings
from ..schemas.sns_share import (
SnsShareSubmit, SnsShareResponse, SnsShareListResponse,
SnsShareVerify, SnsShareStats
)
from .auth import get_current_user, get_current_admin_user
router = APIRouter(prefix="/sns-share", tags=["SNS Share"])
def get_car_info(car: Car) -> dict:
"""차량 정보 추출"""
car_name = f"{car.maker_name or ''} {car.model_name or ''} {car.grade_name or ''}".strip()
car_image = None
if car.images and len(car.images) > 0:
car_image = car.images[0].image_url
return {"car_name": car_name, "car_image": car_image}
def submission_to_response(submission: SnsShareSubmission) -> SnsShareResponse:
"""SnsShareSubmission을 Response로 변환"""
car_info = get_car_info(submission.car) if submission.car else {}
return SnsShareResponse(
id=submission.id,
user_id=submission.user_id,
car_id=submission.car_id,
platform=submission.platform,
sns_url=submission.sns_url,
status=submission.status,
rejected_reason=submission.rejected_reason,
reward_cc=submission.reward_cc,
rewarded_at=submission.rewarded_at,
submitted_at=submission.submitted_at,
verified_at=submission.verified_at,
verified_by=submission.verified_by,
car_name=car_info.get("car_name"),
car_image=car_info.get("car_image"),
)
@router.post("/submit", response_model=SnsShareResponse)
async def submit_sns_share(
data: SnsShareSubmit,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""SNS 공유 URL 제출"""
# 마케팅 캠페인 활성화 확인
settings = db.query(SystemSettings).first()
if settings and not settings.marketing_enabled:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Marketing campaign is not active"
)
# 캠페인 기간 확인
now = datetime.utcnow()
if settings:
if settings.marketing_start_date and now < settings.marketing_start_date:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Marketing campaign has not started yet"
)
if settings.marketing_end_date and now > settings.marketing_end_date:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Marketing campaign has ended"
)
# 차량 존재 확인
car = db.query(Car).filter(Car.id == data.car_id).first()
if not car:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Car not found"
)
# 플랫폼 검증
valid_platforms = ["twitter", "instagram", "facebook"]
if data.platform.lower() not in valid_platforms:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid platform. Must be one of: {', '.join(valid_platforms)}"
)
# 중복 제출 확인 (같은 차량, 같은 URL)
existing = db.query(SnsShareSubmission).filter(
SnsShareSubmission.user_id == current_user.id,
SnsShareSubmission.car_id == data.car_id,
SnsShareSubmission.sns_url == data.sns_url
).first()
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="You have already submitted this URL for this car"
)
# 보상 CC 설정
reward_cc = settings.sns_share_reward_cc if settings else 3.0
# 제출 생성
submission = SnsShareSubmission(
user_id=current_user.id,
car_id=data.car_id,
platform=data.platform.lower(),
sns_url=data.sns_url,
status="pending",
reward_cc=reward_cc,
)
db.add(submission)
db.commit()
db.refresh(submission)
return submission_to_response(submission)
@router.get("/my-submissions", response_model=SnsShareListResponse)
async def get_my_submissions(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""내 SNS 공유 제출 목록"""
submissions = db.query(SnsShareSubmission).filter(
SnsShareSubmission.user_id == current_user.id
).order_by(SnsShareSubmission.submitted_at.desc()).all()
pending_count = len([s for s in submissions if s.status == "pending"])
approved_count = len([s for s in submissions if s.status == "approved"])
rejected_count = len([s for s in submissions if s.status == "rejected"])
return SnsShareListResponse(
submissions=[submission_to_response(s) for s in submissions],
total=len(submissions),
pending_count=pending_count,
approved_count=approved_count,
rejected_count=rejected_count,
)
@router.get("/admin/list", response_model=SnsShareListResponse)
async def admin_get_submissions(
status_filter: Optional[str] = None,
skip: int = 0,
limit: int = 50,
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin_user)
):
"""관리자: 전체 SNS 공유 제출 목록"""
query = db.query(SnsShareSubmission)
if status_filter:
query = query.filter(SnsShareSubmission.status == status_filter)
total = query.count()
submissions = query.order_by(SnsShareSubmission.submitted_at.desc()).offset(skip).limit(limit).all()
# 전체 통계
pending_count = db.query(SnsShareSubmission).filter(SnsShareSubmission.status == "pending").count()
approved_count = db.query(SnsShareSubmission).filter(SnsShareSubmission.status == "approved").count()
rejected_count = db.query(SnsShareSubmission).filter(SnsShareSubmission.status == "rejected").count()
return SnsShareListResponse(
submissions=[submission_to_response(s) for s in submissions],
total=total,
pending_count=pending_count,
approved_count=approved_count,
rejected_count=rejected_count,
)
@router.get("/admin/stats", response_model=SnsShareStats)
async def admin_get_stats(
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin_user)
):
"""관리자: SNS 공유 통계"""
total = db.query(SnsShareSubmission).count()
pending = db.query(SnsShareSubmission).filter(SnsShareSubmission.status == "pending").count()
approved = db.query(SnsShareSubmission).filter(SnsShareSubmission.status == "approved").count()
rejected = db.query(SnsShareSubmission).filter(SnsShareSubmission.status == "rejected").count()
total_rewarded = db.query(func.sum(SnsShareSubmission.reward_cc)).filter(
SnsShareSubmission.status == "approved"
).scalar() or 0.0
return SnsShareStats(
total_submissions=total,
pending_submissions=pending,
approved_submissions=approved,
rejected_submissions=rejected,
total_rewarded_cc=total_rewarded,
)
@router.put("/admin/{submission_id}/verify", response_model=SnsShareResponse)
async def admin_verify_submission(
submission_id: int,
data: SnsShareVerify,
db: Session = Depends(get_db),
admin: User = Depends(get_current_admin_user)
):
"""관리자: SNS 공유 검증 (승인/거부)"""
submission = db.query(SnsShareSubmission).filter(
SnsShareSubmission.id == submission_id
).first()
if not submission:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Submission not found"
)
if submission.status != "pending":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Submission has already been verified"
)
now = datetime.utcnow()
if data.approved:
# 승인: CC 지급
submission.status = "approved"
submission.verified_at = now
submission.verified_by = admin.id
submission.rewarded_at = now
# 사용자 CC 잔액 업데이트 (프로모션 CC - 출금 불가)
user = db.query(User).filter(User.id == submission.user_id).first()
if user:
user.cc_balance += submission.reward_cc
else:
# 거부
submission.status = "rejected"
submission.verified_at = now
submission.verified_by = admin.id
submission.rejected_reason = data.rejected_reason
db.commit()
db.refresh(submission)
return submission_to_response(submission)
@router.get("/campaign-status")
async def get_campaign_status(db: Session = Depends(get_db)):
"""마케팅 캠페인 상태 조회 (공개)"""
settings = db.query(SystemSettings).first()
if not settings:
return {
"enabled": False,
"message": "Campaign not configured"
}
now = datetime.utcnow()
is_active = settings.marketing_enabled
if settings.marketing_start_date and now < settings.marketing_start_date:
is_active = False
if settings.marketing_end_date and now > settings.marketing_end_date:
is_active = False
return {
"enabled": is_active,
"start_date": settings.marketing_start_date,
"end_date": settings.marketing_end_date,
"sns_share_reward_cc": settings.sns_share_reward_cc,
"referral_signup_reward_cc": settings.referral_signup_reward_cc,
}