- 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>
278 lines
9.4 KiB
Python
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,
|
|
}
|