diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py index 4b36fcc..b536b03 100644 --- a/backend/app/api/auth.py +++ b/backend/app/api/auth.py @@ -7,7 +7,7 @@ from pydantic import BaseModel from typing import Optional import bcrypt from ..database import get_db -from ..models import User +from ..models import User, SystemSettings, ReferralReward from ..models.user import generate_referral_code from ..schemas import UserCreate, UserUpdate, UserResponse, Token from ..config import get_settings @@ -151,6 +151,32 @@ def register(user_data: UserCreate, db: Session = Depends(get_db)): db.add(user) db.commit() db.refresh(user) + + # 레퍼럴 가입 보상 처리 + if user.referred_by: + # 추천인 찾기 + referrer = db.query(User).filter(User.referral_code == user.referred_by).first() + if referrer: + # 설정에서 보상 CC 가져오기 + sys_settings = db.query(SystemSettings).first() + reward_cc = sys_settings.referral_signup_reward_cc if sys_settings else 10.0 + + # 추천인에게 CC 지급 (프로모션 CC - 출금 불가) + referrer.cc_balance += reward_cc + + # 레퍼럴 보상 기록 생성 + referral_reward = ReferralReward( + referrer_id=referrer.id, + referred_user_id=user.id, + reward_type="signup", + payment_amount=0.0, + reward_amount=reward_cc, + status="credited", + credited_at=datetime.utcnow(), + ) + db.add(referral_reward) + db.commit() + return user diff --git a/backend/app/api/sns_share.py b/backend/app/api/sns_share.py new file mode 100644 index 0000000..ae4149c --- /dev/null +++ b/backend/app/api/sns_share.py @@ -0,0 +1,277 @@ +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, + } diff --git a/backend/app/main.py b/backend/app/main.py index b766887..e26f941 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -9,7 +9,7 @@ import asyncio from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.cron import CronTrigger from .database import engine, Base, SessionLocal -from .api import cars, auth, inquiries, hero_banners, carmodoo, translations, cc, settings, vehicle_requests, dealer, vehicle_share, withdrawal, referral, notification, dashboard, push, exchange_rate, verification, visitor +from .api import cars, auth, inquiries, hero_banners, carmodoo, translations, cc, settings, vehicle_requests, dealer, vehicle_share, withdrawal, referral, notification, dashboard, push, exchange_rate, verification, visitor, sns_share from .config import get_settings from .services.exchange_rate_service import update_exchange_rates from .services.visitor_service import aggregate_daily_stats, cleanup_old_visitor_logs @@ -201,6 +201,7 @@ app.include_router(push.router, prefix="/api") app.include_router(exchange_rate.router) app.include_router(verification.router, prefix="/api") app.include_router(visitor.router, prefix="/api") +app.include_router(sns_share.router, prefix="/api") @app.get("/") diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index e51b5d7..2ee2c8d 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -11,6 +11,7 @@ from .vehicle_share import VehicleShare, ShareReward from .withdrawal import WithdrawalRequest from .referral import ReferralReward from .notification import Notification +from .sns_share import SnsShareSubmission from .push_subscription import PushSubscription, UserNotificationPreference from .performance_check import CarPerformanceCheck from .car_specification import CarSpecification @@ -52,6 +53,7 @@ __all__ = [ "WithdrawalRequest", "ReferralReward", "Notification", + "SnsShareSubmission", "PushSubscription", "UserNotificationPreference", "ExchangeRate", diff --git a/backend/app/models/referral.py b/backend/app/models/referral.py index 147ce9e..79fa892 100644 --- a/backend/app/models/referral.py +++ b/backend/app/models/referral.py @@ -16,10 +16,13 @@ class ReferralReward(Base): # 피추천인 (추천받아 가입한 사람) referred_user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) - # 결제 금액 (피추천인이 충전한 금액 USD) - payment_amount = Column(Float, nullable=False) + # 보상 유형: "signup" (가입 시), "payment" (결제 시) + reward_type = Column(String(20), default="payment") - # 보상 금액 (결제 금액의 X%) + # 결제 금액 (피추천인이 충전한 금액 USD, signup인 경우 0) + payment_amount = Column(Float, default=0.0) + + # 보상 금액 (결제 금액의 X% 또는 고정 CC) reward_amount = Column(Float, nullable=False) # 보상 상태: pending(대기), credited(적립), withdrawn(출금) diff --git a/backend/app/models/settings.py b/backend/app/models/settings.py index 92b8b28..ca818f9 100644 --- a/backend/app/models/settings.py +++ b/backend/app/models/settings.py @@ -47,6 +47,22 @@ class SystemSettings(Base): exchange_rate_weight_rub = Column(Float, default=0.0) # RUB (러시아 루블) 가중치 exchange_rate_weight_cny = Column(Float, default=0.0) # CNY (중국 위안) 가중치 + # 마케팅 캠페인 설정 + marketing_enabled = Column(Boolean, default=False) # 마케팅 캠페인 활성화 + marketing_start_date = Column(DateTime(timezone=True), nullable=True) # 캠페인 시작일 + marketing_end_date = Column(DateTime(timezone=True), nullable=True) # 캠페인 종료일 + + # SNS 공유 보상 설정 + sns_share_reward_cc = Column(Float, default=3.0) # SNS 공유 보상 CC (기본 3CC) + referral_signup_reward_cc = Column(Float, default=10.0) # 레퍼럴 가입 보상 CC (기본 10CC) + + # 이벤트 CC 유효기간 (개월) + event_cc_validity_months = Column(Integer, default=6) # 이벤트 CC 유효기간 (기본 6개월) + + # 출금 설정 + withdrawal_enabled = Column(Boolean, default=True) # 출금 기능 활성화 + min_withdrawal_usd = Column(Float, default=10.0) # 최소 출금 금액 (USD) + # 타임스탬프 created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) diff --git a/backend/app/models/sns_share.py b/backend/app/models/sns_share.py new file mode 100644 index 0000000..e2d40f2 --- /dev/null +++ b/backend/app/models/sns_share.py @@ -0,0 +1,35 @@ +from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, Text +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from ..database import Base + + +class SnsShareSubmission(Base): + """SNS 공유 제출 및 검증 모델""" + __tablename__ = "sns_share_submissions" + + 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) + + # SNS 정보 + platform = Column(String(20), nullable=False) # twitter, instagram, facebook + sns_url = Column(String(500), nullable=False) # 공유 게시물 URL + + # 상태 + status = Column(String(20), default="pending") # pending, approved, rejected + rejected_reason = Column(Text, nullable=True) # 거부 사유 + + # CC 보상 + reward_cc = Column(Float, default=3.0) # 기본 3CC 보상 + rewarded_at = Column(DateTime(timezone=True), nullable=True) # 보상 지급 시각 + + # 타임스탬프 + submitted_at = Column(DateTime(timezone=True), server_default=func.now()) + verified_at = Column(DateTime(timezone=True), nullable=True) # 검증 시각 + verified_by = Column(Integer, ForeignKey("users.id"), nullable=True) # 검증한 관리자 + + # Relationships + user = relationship("User", foreign_keys=[user_id], backref="sns_submissions") + car = relationship("Car", backref="sns_shares") + verifier = relationship("User", foreign_keys=[verified_by]) diff --git a/backend/app/models/user.py b/backend/app/models/user.py index 944218f..d65c802 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -24,7 +24,8 @@ class User(Base): 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 + cc_balance = Column(Float, default=3.0) # 프로모션 CC (출금 불가) - 가입보너스, SNS공유, 레퍼럴 + cash_cc_balance = Column(Float, default=0.0) # 현금성 CC (출금 가능) - 딜러수수료 88% 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 diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index 986695e..5fac6f8 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -47,6 +47,10 @@ from .notification import ( NotificationCreate, NotificationResponse, NotificationListResponse, NotificationMarkRead, ) +from .sns_share import ( + SnsShareSubmit, SnsShareResponse, SnsShareListResponse, + SnsShareVerify, SnsShareStats, +) __all__ = [ "CarMakerCreate", "CarMakerResponse", @@ -78,4 +82,6 @@ __all__ = [ "ReferralSettingsResponse", "ReferralSettingsUpdate", "NotificationCreate", "NotificationResponse", "NotificationListResponse", "NotificationMarkRead", + "SnsShareSubmit", "SnsShareResponse", "SnsShareListResponse", + "SnsShareVerify", "SnsShareStats", ] diff --git a/backend/app/schemas/settings.py b/backend/app/schemas/settings.py index 77405a4..a9f941d 100644 --- a/backend/app/schemas/settings.py +++ b/backend/app/schemas/settings.py @@ -22,6 +22,16 @@ class SystemSettingsUpdate(BaseModel): referral_reward_percent: Optional[float] = None referral_reward_type: Optional[str] = None + # 마케팅 캠페인 설정 + marketing_enabled: Optional[bool] = None + marketing_start_date: Optional[datetime] = None + marketing_end_date: Optional[datetime] = None + sns_share_reward_cc: Optional[float] = None + referral_signup_reward_cc: Optional[float] = None + event_cc_validity_months: Optional[int] = None + withdrawal_enabled: Optional[bool] = None + min_withdrawal_usd: Optional[float] = None + class SystemSettingsResponse(BaseModel): """시스템 설정 응답 스키마""" @@ -42,6 +52,17 @@ class SystemSettingsResponse(BaseModel): referral_reward_enabled: bool = True referral_reward_percent: float = 10.0 referral_reward_type: str = "one_time" + + # 마케팅 캠페인 설정 + marketing_enabled: bool = False + marketing_start_date: Optional[datetime] = None + marketing_end_date: Optional[datetime] = None + sns_share_reward_cc: float = 3.0 + referral_signup_reward_cc: float = 10.0 + event_cc_validity_months: int = 6 + withdrawal_enabled: bool = True + min_withdrawal_usd: float = 10.0 + created_at: Optional[datetime] = None updated_at: Optional[datetime] = None diff --git a/backend/app/schemas/sns_share.py b/backend/app/schemas/sns_share.py new file mode 100644 index 0000000..22b90f4 --- /dev/null +++ b/backend/app/schemas/sns_share.py @@ -0,0 +1,57 @@ +from pydantic import BaseModel, HttpUrl +from typing import Optional, List +from datetime import datetime + + +class SnsShareSubmit(BaseModel): + """SNS 공유 제출 요청""" + car_id: int + platform: str # twitter, instagram, facebook + sns_url: str # 공유 게시물 URL + + +class SnsShareResponse(BaseModel): + """SNS 공유 제출 응답""" + id: int + user_id: int + car_id: int + platform: str + sns_url: str + status: str + rejected_reason: Optional[str] = None + reward_cc: float + rewarded_at: Optional[datetime] = None + submitted_at: datetime + verified_at: Optional[datetime] = None + verified_by: Optional[int] = None + + # Car info for display + car_name: Optional[str] = None + car_image: Optional[str] = None + + class Config: + from_attributes = True + + +class SnsShareListResponse(BaseModel): + """SNS 공유 목록 응답""" + submissions: List[SnsShareResponse] + total: int + pending_count: int + approved_count: int + rejected_count: int + + +class SnsShareVerify(BaseModel): + """관리자 검증 요청""" + approved: bool + rejected_reason: Optional[str] = None + + +class SnsShareStats(BaseModel): + """SNS 공유 통계""" + total_submissions: int + pending_submissions: int + approved_submissions: int + rejected_submissions: int + total_rewarded_cc: float diff --git a/backend/migrations/marketing_campaign_v1.sql b/backend/migrations/marketing_campaign_v1.sql new file mode 100644 index 0000000..8cc726a --- /dev/null +++ b/backend/migrations/marketing_campaign_v1.sql @@ -0,0 +1,45 @@ +-- Marketing Campaign Migration V1 +-- Apply to staging/production PostgreSQL databases +-- +-- Instructions: +-- 1. Connect to staging: psql -U autonet -d autonet_staging +-- 2. Run: \i /path/to/marketing_campaign_v1.sql +-- + +-- 1. Add cash_cc_balance to users table +ALTER TABLE users ADD COLUMN IF NOT EXISTS cash_cc_balance FLOAT DEFAULT 0.0; + +-- 2. Create sns_share_submissions table +CREATE TABLE IF NOT EXISTS sns_share_submissions ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id), + car_id INTEGER NOT NULL REFERENCES cars(id), + platform VARCHAR(20) NOT NULL, + sns_url VARCHAR(500) NOT NULL, + status VARCHAR(20) DEFAULT 'pending', + rejected_reason TEXT, + reward_cc FLOAT DEFAULT 3.0, + rewarded_at TIMESTAMP WITH TIME ZONE, + submitted_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + verified_at TIMESTAMP WITH TIME ZONE, + verified_by INTEGER REFERENCES users(id) +); + +CREATE INDEX IF NOT EXISTS ix_sns_share_submissions_user_id ON sns_share_submissions(user_id); +CREATE INDEX IF NOT EXISTS ix_sns_share_submissions_status ON sns_share_submissions(status); + +-- 3. Add marketing settings to system_settings table +ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS marketing_enabled BOOLEAN DEFAULT FALSE; +ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS marketing_start_date TIMESTAMP WITH TIME ZONE; +ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS marketing_end_date TIMESTAMP WITH TIME ZONE; +ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS sns_share_reward_cc FLOAT DEFAULT 3.0; +ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS referral_signup_reward_cc FLOAT DEFAULT 10.0; +ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS event_cc_validity_months INTEGER DEFAULT 6; +ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS withdrawal_enabled BOOLEAN DEFAULT TRUE; +ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS min_withdrawal_usd FLOAT DEFAULT 10.0; + +-- 4. Add reward_type to referral_rewards table +ALTER TABLE referral_rewards ADD COLUMN IF NOT EXISTS reward_type VARCHAR(20) DEFAULT 'payment'; + +-- Done +SELECT 'Marketing Campaign V1 migration completed!' AS status; diff --git a/frontend/src/app/admin/layout.tsx b/frontend/src/app/admin/layout.tsx index 9e0dac7..ce39fb2 100644 --- a/frontend/src/app/admin/layout.tsx +++ b/frontend/src/app/admin/layout.tsx @@ -14,6 +14,7 @@ const menuItems = [ { href: '/admin/dealers', label: 'Dealers', icon: '🤝' }, { href: '/admin/payments', label: 'Payments', icon: '💳' }, { href: '/admin/withdrawals', label: 'Withdrawals', icon: '💸' }, + { href: '/admin/sns-shares', label: 'SNS Shares', icon: '📢' }, { href: '/admin/notifications', label: 'Notifications', icon: '🔔' }, { href: '/admin/translations', label: 'Translations', icon: '🌐' }, { href: '/admin/dealer-translations', label: 'Dealer Descriptions', icon: '📝' }, diff --git a/frontend/src/app/admin/settings/page.tsx b/frontend/src/app/admin/settings/page.tsx index ee87b70..a24c9b8 100644 --- a/frontend/src/app/admin/settings/page.tsx +++ b/frontend/src/app/admin/settings/page.tsx @@ -24,6 +24,15 @@ interface SystemSettings { exchange_rate_weight_mnt: number; exchange_rate_weight_rub: number; exchange_rate_weight_cny: number; + // Marketing campaign settings + marketing_enabled: boolean; + marketing_start_date: string | null; + marketing_end_date: string | null; + sns_share_reward_cc: number; + referral_signup_reward_cc: number; + event_cc_validity_months: number; + withdrawal_enabled: boolean; + min_withdrawal_usd: number; } interface ExchangeRateWeights { @@ -66,6 +75,15 @@ export default function SettingsPage() { referral_reward_enabled: true, referral_reward_percent: 10.0, referral_reward_type: 'one_time', + // Marketing campaign + marketing_enabled: false, + marketing_start_date: '', + marketing_end_date: '', + sns_share_reward_cc: 3.0, + referral_signup_reward_cc: 10.0, + event_cc_validity_months: 6, + withdrawal_enabled: true, + min_withdrawal_usd: 10.0, }); useEffect(() => { @@ -96,6 +114,15 @@ export default function SettingsPage() { referral_reward_enabled: data.referral_reward_enabled ?? true, referral_reward_percent: data.referral_reward_percent ?? 10.0, referral_reward_type: data.referral_reward_type || 'one_time', + // Marketing campaign + marketing_enabled: data.marketing_enabled ?? false, + marketing_start_date: data.marketing_start_date ? data.marketing_start_date.split('T')[0] : '', + marketing_end_date: data.marketing_end_date ? data.marketing_end_date.split('T')[0] : '', + sns_share_reward_cc: data.sns_share_reward_cc ?? 3.0, + referral_signup_reward_cc: data.referral_signup_reward_cc ?? 10.0, + event_cc_validity_months: data.event_cc_validity_months ?? 6, + withdrawal_enabled: data.withdrawal_enabled ?? true, + min_withdrawal_usd: data.min_withdrawal_usd ?? 10.0, }); } } catch (error) { @@ -531,6 +558,152 @@ export default function SettingsPage() { + {/* Marketing Campaign Settings */} +
SNS 공유 및 레퍼럴 보상 캠페인 활성화
+SNS 공유 승인 시 지급되는 CC
+친구가 가입 시 추천인에게 지급되는 CC
+이벤트 CC 유효기간
+SNS Share: {formData.sns_share_reward_cc} CC per approved share
+Referral Signup: {formData.referral_signup_reward_cc} CC per new signup
+Period: {formData.marketing_start_date || 'Not set'} ~ {formData.marketing_end_date || 'Not set'}
+