diff --git a/backend/app/api/vehicle_requests.py b/backend/app/api/vehicle_requests.py index ba580a7..9e8d1de 100644 --- a/backend/app/api/vehicle_requests.py +++ b/backend/app/api/vehicle_requests.py @@ -4,12 +4,12 @@ from typing import List from datetime import datetime, timedelta from ..database import get_db -from ..models import VehicleRequest, RequestVehicle, PurchasedVehicle, User, DealerInfo, SystemSettings, Car +from ..models import VehicleRequest, RequestVehicle, PurchasedVehicle, User, DealerInfo, SystemSettings, Car, CarView from ..schemas import ( VehicleRequestCreate, VehicleRequestResponse, RequestVehicleCreate, RequestVehicleResponse, RequestVehicleApprove, PurchasedVehicleCreate, PurchasedVehicleResponse, PurchasedVehicleUpdateStatus, - VehicleRequestWithVehicles, + VehicleRequestWithVehicles, DirectPurchasedCarResponse, MyVehiclesResponse, ) from .auth import get_current_user from .notification import notify_vehicle_recommended, notify_shipping_update @@ -120,6 +120,97 @@ def get_my_requests( return result +@router.get("/my-vehicles", response_model=MyVehiclesResponse) +def get_my_vehicles( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Get all vehicles accessible to user: recommended + directly purchased from banners""" + + # 1. Get vehicle requests with recommended vehicles (existing logic) + requests = db.query(VehicleRequest).filter( + VehicleRequest.user_id == current_user.id + ).order_by(VehicleRequest.created_at.desc()).all() + + vehicle_requests = [] + for req in requests: + if DEV_MODE or (req.created_at and datetime.utcnow() - req.created_at > timedelta(hours=24)): + approved_vehicles = [v for v in req.recommended_vehicles if v.is_approved] + else: + approved_vehicles = [] + + enriched_vehicles = [] + for v in approved_vehicles: + vehicle_response = RequestVehicleResponse.model_validate(v) + if v.car_id: + car = db.query(Car).filter(Car.id == v.car_id).first() + if car: + vehicle_response.car_data = {**vehicle_response.car_data, "soldout": car.soldout} + enriched_vehicles.append(vehicle_response) + + vehicle_requests.append(VehicleRequestWithVehicles( + request=VehicleRequestResponse.model_validate(req), + approved_vehicles=enriched_vehicles + )) + + # 2. Get directly purchased cars (from CarView - paid 1CC on banner) + car_views = db.query(CarView).filter( + CarView.user_id == current_user.id + ).order_by(CarView.created_at.desc()).all() + + # Get the car_ids that are already in recommended vehicles (to avoid duplicates) + recommended_car_ids = set() + for vr in vehicle_requests: + for v in vr.approved_vehicles: + if v.car_id: + recommended_car_ids.add(v.car_id) + + direct_purchases = [] + for cv in car_views: + # Skip if already in recommended (user got recommendation first, then it's not a "direct" purchase) + if cv.car_id in recommended_car_ids: + continue + + car = db.query(Car).filter(Car.id == cv.car_id).first() + if car: + # Build car_data similar to recommended vehicles + main_image = None + if car.images: + main_img = next((img for img in car.images if img.is_main), None) + if main_img: + main_image = main_img.url + elif car.images: + main_image = car.images[0].url + + car_data = { + "id": str(car.source_id) if car.source_id else str(car.id), + "car_name": car.car_name, + "maker_name": car.maker.name if car.maker else None, + "year": car.year, + "mileage": car.mileage, + "final_price": car.final_price_krw, + "fuel": car.fuel, + "transmission": car.transmission, + "color": car.color, + "main_image": main_image, + "soldout": car.soldout, + "local_car_id": car.id, + } + + direct_purchases.append(DirectPurchasedCarResponse( + id=cv.id, + car_id=cv.car_id, + car_data=car_data, + cc_paid=cv.cc_paid, + purchased_at=cv.created_at + )) + + return MyVehiclesResponse( + vehicle_requests=vehicle_requests, + direct_purchases=direct_purchases + ) + + # ===================== # Purchased Vehicles (Find My Car) # ===================== diff --git a/backend/app/schemas/vehicle_request.py b/backend/app/schemas/vehicle_request.py index 49e07c0..c891c5a 100644 --- a/backend/app/schemas/vehicle_request.py +++ b/backend/app/schemas/vehicle_request.py @@ -123,3 +123,21 @@ class VehicleRequestWithVehicles(BaseModel): class Config: from_attributes = True + + +# Directly purchased car (from CarView - when user pays 1CC on banner car) +class DirectPurchasedCarResponse(BaseModel): + id: int # CarView id + car_id: int + car_data: dict # Car details for display + cc_paid: float + purchased_at: datetime + + class Config: + from_attributes = True + + +# Full response including both recommended and directly purchased cars +class MyVehiclesResponse(BaseModel): + vehicle_requests: List[VehicleRequestWithVehicles] + direct_purchases: List[DirectPurchasedCarResponse] # Cars purchased directly from banner diff --git a/frontend/src/app/my-request/page.tsx b/frontend/src/app/my-request/page.tsx index cb8901f..05e9239 100644 --- a/frontend/src/app/my-request/page.tsx +++ b/frontend/src/app/my-request/page.tsx @@ -6,7 +6,7 @@ import Link from 'next/link'; import Image from 'next/image'; import { useTranslation, formatPriceWithCurrency, translateCarName } from '@/lib/i18n'; import { useAuthStore } from '@/lib/store'; -import { vehicleRequestsApi, VehicleRequestWithVehicles } from '@/lib/api'; +import { vehicleRequestsApi, VehicleRequestWithVehicles, DirectPurchasedCar } from '@/lib/api'; import SidebarLayout from '@/components/SidebarLayout'; export default function MyRequestPage() { @@ -15,9 +15,11 @@ export default function MyRequestPage() { const { user } = useAuthStore(); const [requests, setRequests] = useState([]); + const [directPurchases, setDirectPurchases] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [expandedRequest, setExpandedRequest] = useState(null); + const [showDirectPurchases, setShowDirectPurchases] = useState(true); // Redirect if not logged in useEffect(() => { @@ -26,28 +28,29 @@ export default function MyRequestPage() { } }, [user, router]); - // Load requests + // Load all vehicles (recommended + directly purchased) useEffect(() => { - const loadRequests = async () => { + const loadVehicles = async () => { if (!user) return; try { setIsLoading(true); - const data = await vehicleRequestsApi.getMyRequests(); - setRequests(data); + const data = await vehicleRequestsApi.getMyVehicles(); + setRequests(data.vehicle_requests); + setDirectPurchases(data.direct_purchases); // Auto-expand first request if it has approved vehicles - if (data.length > 0 && data[0].approved_vehicles.length > 0) { - setExpandedRequest(data[0].request.id); + if (data.vehicle_requests.length > 0 && data.vehicle_requests[0].approved_vehicles.length > 0) { + setExpandedRequest(data.vehicle_requests[0].request.id); } } catch (err) { - console.error('Failed to load requests:', err); - setError(language === 'ko' ? '요청 목록을 불러오는데 실패했습니다.' : 'Failed to load requests.'); + console.error('Failed to load vehicles:', err); + setError(language === 'ko' ? '차량 목록을 불러오는데 실패했습니다.' : 'Failed to load vehicles.'); } finally { setIsLoading(false); } }; - loadRequests(); + loadVehicles(); }, [user, language]); // Format date (mn uses en-US as mn-MN is not supported in most browsers) @@ -130,8 +133,8 @@ export default function MyRequestPage() { )} - {/* No Requests */} - {!isLoading && !error && requests.length === 0 && ( + {/* No Vehicles at all */} + {!isLoading && !error && requests.length === 0 && directPurchases.length === 0 && (
@@ -338,6 +341,169 @@ export default function MyRequestPage() { ))}
)} + + {/* Directly Purchased Cars Section */} + {!isLoading && !error && directPurchases.length > 0 && ( +
+
+ {/* Section Header */} +
setShowDirectPurchases(!showDirectPurchases)} + > +
+
+
+ + + +
+
+

+ {language === 'ko' ? '직접 구매한 차량' : 'Directly Purchased Cars'} +

+

+ {language === 'ko' ? '배너에서 1CC로 정보를 구매한 차량' : 'Cars purchased with 1CC from banners'} +

+
+
+
+ + {directPurchases.length} {language === 'ko' ? '대' : 'cars'} + + + + +
+
+
+ + {/* Direct Purchases Grid */} + {showDirectPurchases && ( +
+
+ {directPurchases.map((purchase) => { + const carData = purchase.car_data; + const priceInfo = formatPrice(carData?.final_price); + const isSoldout = carData?.soldout === true; + const carId = purchase.car_id; + + return ( +
+ {/* Clickable Vehicle Card */} + + {/* Vehicle Image */} +
+ {carData?.main_image ? ( + {carData.car_name + ) : ( +
+ + + +
+ )} + {/* Soldout Badge */} + {isSoldout && ( +
+ + {language === 'ko' ? '판매완료' : 'SOLD OUT'} + +
+ )} + {/* Direct Purchase Badge */} +
+ + {purchase.cc_paid} CC + +
+
+ + {/* Vehicle Info */} +
+
+ {translateCarName(carData?.car_name, language)} +
+ +
+
+ {t.year} + {carData?.year || '-'} +
+
+ {t.mileage} + {carData?.mileage?.toLocaleString()} km +
+
+ {t.fuel} + {translateCarName(carData?.fuel, language) || '-'} +
+
+ +
+
+ {priceInfo.usdt} +
+
+ {priceInfo.local} +
+
+
+ + + {/* Action Buttons */} +
+ {isSoldout ? ( +
+ {language === 'ko' ? '판매완료' : 'Sold Out'} +
+ ) : ( + <> + + + + )} +
+
+ ); + })} +
+
+ )} +
+
+ )}
); diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 16f24cb..dcd93ca 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -630,6 +630,21 @@ export interface VehicleRequestWithVehicles { approved_vehicles: RequestVehicle[]; } +// Directly purchased car (from banner with 1CC) +export interface DirectPurchasedCar { + id: number; + car_id: number; + car_data: Record; + cc_paid: number; + purchased_at: string; +} + +// Full response including both recommended and directly purchased cars +export interface MyVehiclesResponse { + vehicle_requests: VehicleRequestWithVehicles[]; + direct_purchases: DirectPurchasedCar[]; +} + export const vehicleRequestsApi = { // User endpoints createRequest: async (requestData: { @@ -656,6 +671,12 @@ export const vehicleRequestsApi = { return data; }, + // Get all vehicles: recommended + directly purchased from banners + getMyVehicles: async (): Promise => { + const { data } = await api.get('/vehicle-requests/my-vehicles'); + return data; + }, + getPurchasedVehicles: async (): Promise => { const { data } = await api.get('/vehicle-requests/purchased'); return data;