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:
363
backend/app/api/notification.py
Normal file
363
backend/app/api/notification.py
Normal file
@@ -0,0 +1,363 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import User, Notification
|
||||
from ..schemas.notification import (
|
||||
NotificationCreate, NotificationResponse,
|
||||
NotificationListResponse, NotificationMarkRead
|
||||
)
|
||||
from .auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/notifications", tags=["notifications"])
|
||||
|
||||
|
||||
# =====================
|
||||
# Helper Functions
|
||||
# =====================
|
||||
|
||||
def create_notification(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
notification_type: str,
|
||||
title: str,
|
||||
message: str,
|
||||
link: Optional[str] = None,
|
||||
related_id: Optional[int] = None,
|
||||
related_type: Optional[str] = None
|
||||
) -> Notification:
|
||||
"""Create a new notification"""
|
||||
notification = Notification(
|
||||
user_id=user_id,
|
||||
notification_type=notification_type,
|
||||
title=title,
|
||||
message=message,
|
||||
link=link,
|
||||
related_id=related_id,
|
||||
related_type=related_type
|
||||
)
|
||||
db.add(notification)
|
||||
db.commit()
|
||||
db.refresh(notification)
|
||||
return notification
|
||||
|
||||
|
||||
def notify_vehicle_recommended(db: Session, user_id: int, request_id: int, vehicle_count: int):
|
||||
"""Notify user when vehicles are recommended for their request"""
|
||||
return create_notification(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
notification_type="vehicle_recommended",
|
||||
title="차량 추천 완료",
|
||||
message=f"{vehicle_count}대의 차량이 추천되었습니다. 지금 확인해보세요!",
|
||||
link=f"/my-request",
|
||||
related_id=request_id,
|
||||
related_type="vehicle_request"
|
||||
)
|
||||
|
||||
|
||||
def notify_shipping_update(db: Session, user_id: int, vehicle_id: int, status: int, car_name: str):
|
||||
"""Notify user when shipping status changes"""
|
||||
status_names = {
|
||||
1: "구매완료",
|
||||
2: "인천항 도착",
|
||||
3: "텐진항 도착",
|
||||
4: "자먼우드 도착",
|
||||
5: "울란바토르 도착",
|
||||
6: "통관 진행중",
|
||||
7: "배송완료"
|
||||
}
|
||||
status_name = status_names.get(status, f"상태 {status}")
|
||||
|
||||
return create_notification(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
notification_type="shipping_update",
|
||||
title="배송 상태 업데이트",
|
||||
message=f"{car_name}: {status_name}",
|
||||
link=f"/find-my-car",
|
||||
related_id=vehicle_id,
|
||||
related_type="purchased_vehicle"
|
||||
)
|
||||
|
||||
|
||||
def notify_withdrawal_processed(db: Session, user_id: int, withdrawal_id: int, status: str, amount: float):
|
||||
"""Notify user when withdrawal request is processed"""
|
||||
status_messages = {
|
||||
"approved": f"출금 신청이 승인되었습니다. {amount:,.0f}원이 곧 입금됩니다.",
|
||||
"completed": f"출금 완료! {amount:,.0f}원이 입금되었습니다.",
|
||||
"rejected": "출금 신청이 거부되었습니다. 관리자에게 문의해주세요."
|
||||
}
|
||||
|
||||
return create_notification(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
notification_type="withdrawal_processed",
|
||||
title="출금 처리 알림",
|
||||
message=status_messages.get(status, "출금 상태가 변경되었습니다."),
|
||||
link="/withdrawal",
|
||||
related_id=withdrawal_id,
|
||||
related_type="withdrawal"
|
||||
)
|
||||
|
||||
|
||||
def notify_referral_reward(db: Session, user_id: int, reward_amount: float, referred_name: str):
|
||||
"""Notify user when they receive referral reward"""
|
||||
return create_notification(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
notification_type="referral_reward",
|
||||
title="레퍼럴 보상 적립",
|
||||
message=f"{referred_name}님의 충전으로 {reward_amount:,.0f}원이 적립되었습니다!",
|
||||
link="/withdrawal",
|
||||
related_type="referral"
|
||||
)
|
||||
|
||||
|
||||
def notify_dealer_approved(db: Session, user_id: int, dealer_code: str):
|
||||
"""Notify user when dealer application is approved"""
|
||||
return create_notification(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
notification_type="dealer_approved",
|
||||
title="딜러 승인 완료",
|
||||
message=f"딜러 승인이 완료되었습니다! 딜러 코드: {dealer_code}",
|
||||
link="/dealer/my-card",
|
||||
related_type="dealer"
|
||||
)
|
||||
|
||||
|
||||
def notify_dealer_rejected(db: Session, user_id: int, reason: str):
|
||||
"""Notify user when dealer application is rejected"""
|
||||
return create_notification(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
notification_type="dealer_rejected",
|
||||
title="딜러 신청 거부",
|
||||
message=f"딜러 신청이 거부되었습니다. 사유: {reason}",
|
||||
link="/dealer/apply",
|
||||
related_type="dealer"
|
||||
)
|
||||
|
||||
|
||||
def notify_share_purchased(db: Session, user_id: int, share_id: int, reward_amount: float, car_name: str):
|
||||
"""Notify user when their shared vehicle is purchased"""
|
||||
return create_notification(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
notification_type="share_purchased",
|
||||
title="공유 차량 판매 완료",
|
||||
message=f"{car_name} 판매 완료! 리워드 {reward_amount:,.0f}원이 적립되었습니다.",
|
||||
link="/withdrawal",
|
||||
related_id=share_id,
|
||||
related_type="vehicle_share"
|
||||
)
|
||||
|
||||
|
||||
def notify_payment_confirmed(db: Session, user_id: int, charge_id: int, amount: float, cc_amount: int):
|
||||
"""Notify user when payment is confirmed"""
|
||||
return create_notification(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
notification_type="payment_confirmed",
|
||||
title="결제 확인 완료",
|
||||
message=f"결제가 확인되었습니다! ${amount:.2f} → {cc_amount} CC가 충전되었습니다.",
|
||||
link="/charge",
|
||||
related_id=charge_id,
|
||||
related_type="charge"
|
||||
)
|
||||
|
||||
|
||||
def notify_inquiry_reply(db: Session, user_id: int, inquiry_id: int, subject: str = None):
|
||||
"""Notify user when admin replies to their inquiry"""
|
||||
return create_notification(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
notification_type="inquiry_reply",
|
||||
title="문의 답변 등록",
|
||||
message=f"문의에 답변이 등록되었습니다." + (f" ({subject})" if subject else ""),
|
||||
link=f"/my-inquiries/{inquiry_id}",
|
||||
related_id=inquiry_id,
|
||||
related_type="inquiry"
|
||||
)
|
||||
|
||||
|
||||
def notify_system(db: Session, user_id: int, title: str, message: str, link: Optional[str] = None):
|
||||
"""Send a general system notification to a user"""
|
||||
return create_notification(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
notification_type="system",
|
||||
title=title,
|
||||
message=message,
|
||||
link=link
|
||||
)
|
||||
|
||||
|
||||
# =====================
|
||||
# User Endpoints
|
||||
# =====================
|
||||
|
||||
@router.get("/", response_model=NotificationListResponse)
|
||||
def get_notifications(
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
unread_only: bool = False,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get user's notifications"""
|
||||
query = db.query(Notification).filter(Notification.user_id == current_user.id)
|
||||
|
||||
if unread_only:
|
||||
query = query.filter(Notification.is_read == False)
|
||||
|
||||
total = query.count()
|
||||
unread_count = db.query(Notification).filter(
|
||||
Notification.user_id == current_user.id,
|
||||
Notification.is_read == False
|
||||
).count()
|
||||
|
||||
notifications = query.order_by(desc(Notification.created_at)) \
|
||||
.offset((page - 1) * page_size) \
|
||||
.limit(page_size) \
|
||||
.all()
|
||||
|
||||
return NotificationListResponse(
|
||||
notifications=[NotificationResponse.model_validate(n) for n in notifications],
|
||||
unread_count=unread_count,
|
||||
total=total
|
||||
)
|
||||
|
||||
|
||||
@router.get("/unread-count")
|
||||
def get_unread_count(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get count of unread notifications"""
|
||||
count = db.query(Notification).filter(
|
||||
Notification.user_id == current_user.id,
|
||||
Notification.is_read == False
|
||||
).count()
|
||||
|
||||
return {"unread_count": count}
|
||||
|
||||
|
||||
@router.post("/mark-read")
|
||||
def mark_as_read(
|
||||
data: NotificationMarkRead,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Mark notifications as read"""
|
||||
notifications = db.query(Notification).filter(
|
||||
Notification.id.in_(data.notification_ids),
|
||||
Notification.user_id == current_user.id
|
||||
).all()
|
||||
|
||||
for notification in notifications:
|
||||
notification.is_read = True
|
||||
notification.read_at = datetime.utcnow()
|
||||
|
||||
db.commit()
|
||||
|
||||
return {"message": f"Marked {len(notifications)} notifications as read"}
|
||||
|
||||
|
||||
@router.post("/mark-all-read")
|
||||
def mark_all_as_read(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Mark all notifications as read"""
|
||||
count = db.query(Notification).filter(
|
||||
Notification.user_id == current_user.id,
|
||||
Notification.is_read == False
|
||||
).update({
|
||||
"is_read": True,
|
||||
"read_at": datetime.utcnow()
|
||||
})
|
||||
|
||||
db.commit()
|
||||
|
||||
return {"message": f"Marked {count} notifications as read"}
|
||||
|
||||
|
||||
@router.delete("/{notification_id}")
|
||||
def delete_notification(
|
||||
notification_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Delete a notification"""
|
||||
notification = db.query(Notification).filter(
|
||||
Notification.id == notification_id,
|
||||
Notification.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if not notification:
|
||||
raise HTTPException(status_code=404, detail="Notification not found")
|
||||
|
||||
db.delete(notification)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Notification deleted"}
|
||||
|
||||
|
||||
# =====================
|
||||
# Admin Endpoints
|
||||
# =====================
|
||||
|
||||
@router.post("/admin/send")
|
||||
def admin_send_notification(
|
||||
notification_data: NotificationCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""[Admin] Send notification to a user"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
notification = create_notification(
|
||||
db=db,
|
||||
user_id=notification_data.user_id,
|
||||
notification_type=notification_data.notification_type,
|
||||
title=notification_data.title,
|
||||
message=notification_data.message,
|
||||
link=notification_data.link,
|
||||
related_id=notification_data.related_id,
|
||||
related_type=notification_data.related_type
|
||||
)
|
||||
|
||||
return NotificationResponse.model_validate(notification)
|
||||
|
||||
|
||||
@router.post("/admin/send-all")
|
||||
def admin_send_to_all(
|
||||
title: str,
|
||||
message: str,
|
||||
link: Optional[str] = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""[Admin] Send notification to all users"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
users = db.query(User).filter(User.is_active == True).all()
|
||||
|
||||
for user in users:
|
||||
create_notification(
|
||||
db=db,
|
||||
user_id=user.id,
|
||||
notification_type="system",
|
||||
title=title,
|
||||
message=message,
|
||||
link=link
|
||||
)
|
||||
|
||||
return {"message": f"Sent notification to {len(users)} users"}
|
||||
Reference in New Issue
Block a user