diff --git a/CLAUDE.md b/CLAUDE.md index 4357b12..b4451e1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -134,6 +134,50 @@ ssh server2 "cd /opt/autonet/production/frontend && docker build --no-cache -t p --- +## Staging vs Production 환경 분리 + +### 데이터베이스 분리 + +| 환경 | DB 이름 | 용도 | +|------|---------|------| +| **Production** | `autonet` | 실서비스 데이터 | +| **Staging** | `autonet_staging` | 테스트용 데이터 (안전한 테스트) | + +**DB 서버**: PostgreSQL on `192.168.0.201:5432` + +### Staging 환경 설정 + +**Backend .env** (`/opt/autonet/staging/backend/.env`): +```env +DB_NAME=autonet_staging # Production은 autonet +``` + +**docker-compose.staging.yml**: +- Frontend: `NEXT_PUBLIC_API_URL=https://staging.autonetsellcar.com` (build arg) +- Backend: `.env` 파일을 볼륨 마운트로 연결 + +### 소스 코드 동기화 + +**⚠️ 중요**: Staging 디렉토리는 Git 저장소가 아님! Production과 수동 동기화 필요 + +```bash +# Production → Staging 소스 동기화 +ssh damon@192.168.0.202 "rsync -av --exclude='node_modules' --exclude='.next' /opt/autonet/production/frontend/ /opt/autonet/staging/frontend/" + +# Staging Frontend 재빌드 +ssh damon@192.168.0.202 "cd /opt/autonet/staging && docker compose -f docker-compose.staging.yml build --no-cache frontend-staging && docker compose -f docker-compose.staging.yml up -d frontend-staging" +``` + +### Staging DB 초기 데이터 복사 + +```bash +# Production → Staging 데이터 복사 (psql 사용) +PGPASSWORD='roskfl@1122' pg_dump -h 192.168.0.201 -U admin -d autonet --data-only -t users -t cars -t car_images -t hero_banners | \ +PGPASSWORD='roskfl@1122' psql -h 192.168.0.201 -U admin -d autonet_staging +``` + +--- + ## 1. 프로젝트 구조 ``` @@ -790,6 +834,8 @@ Import 시 자동 번역 (Azure API) | 날짜 | 변경 내용 | |------|----------| +| 2025-01-03 | **Staging DB 분리**: `autonet_staging` DB 생성, Production/Staging 환경 완전 분리 | +| 2025-01-03 | **My Vehicles 기능**: 직접 구매 차량을 My Request 페이지에 표시 | | 2024-12-31 | **Admin Settings 기능 추가**: Show Dealer Comment 토글, Korean Domestic + Export Customs 금액 설정 | | 2024-12-31 | Cost 페이지에서 국내비용+수출통관비용 동적 적용 (settings API 연동) | | 2024-12-31 | **Visitor Stats 국가별 통계 강화**: 국기 이모지 추가, 전용 Country Stats 카드 추가 | diff --git a/backend/app/api/carmodoo.py b/backend/app/api/carmodoo.py index 9d84f9f..97068d9 100644 --- a/backend/app/api/carmodoo.py +++ b/backend/app/api/carmodoo.py @@ -16,7 +16,7 @@ from pathlib import Path from lxml import etree from lxml import html as lxml_html from ..database import get_db -from ..models import Car, CarMaker, CarModel, CarImage, CarOption, CarPerformanceCheck, CarSpecification, PerformanceCheckView, CarView, User +from ..models import Car, CarMaker, CarModel, CarImage, CarOption, CarPerformanceCheck, CarSpecification, PerformanceCheckView, CarView, User, VehicleRequest, RequestVehicle from ..models.settings import SystemSettings from ..api.auth import get_current_user, get_current_user_optional, get_current_admin_user from ..services.cache_service import CacheService @@ -1712,7 +1712,14 @@ async def get_car_performance_check( CarView.car_id == car_id ).first() - has_access = (existing_perf_view is not None) or (existing_car_view is not None) + # Check 3: This car was recommended to the user (paid 1 CC for recommendation) + recommended_vehicle = db.query(RequestVehicle).join(VehicleRequest).filter( + VehicleRequest.user_id == current_user.id, + RequestVehicle.car_id == car_id, + RequestVehicle.is_approved == True + ).first() + + has_access = (existing_perf_view is not None) or (existing_car_view is not None) or (recommended_vehicle is not None) # If no access, return only basic info (that performance check exists) if not has_access: diff --git a/frontend/package.json b/frontend/package.json index a1f186a..ece3980 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,9 @@ "dev": "next dev", "build": "next build", "start": "next start -p 3000", - "lint": "next lint" + "lint": "next lint", + "audit": "npm audit --omit=dev", + "audit:fix": "npm audit fix --omit=dev" }, "dependencies": { "@heroicons/react": "^2.1.1", diff --git a/frontend/src/app/admin/vehicle-requests/page.tsx b/frontend/src/app/admin/vehicle-requests/page.tsx index cc6b7fb..cc3ce50 100644 --- a/frontend/src/app/admin/vehicle-requests/page.tsx +++ b/frontend/src/app/admin/vehicle-requests/page.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import Link from 'next/link'; -import { vehicleRequestsApi, VehicleRequest, VehicleRequestWithVehicles } from '@/lib/api'; +import { vehicleRequestsApi, VehicleRequest, VehicleRequestWithVehicles, adminPdfApi } from '@/lib/api'; export default function AdminVehicleRequestsPage() { const [requests, setRequests] = useState([]); @@ -10,6 +10,7 @@ export default function AdminVehicleRequestsPage() { const [isLoading, setIsLoading] = useState(true); const [statusFilter, setStatusFilter] = useState(''); const [deletingVehicleId, setDeletingVehicleId] = useState(null); + const [regeneratingPdfId, setRegeneratingPdfId] = useState(null); // Load requests useEffect(() => { @@ -89,6 +90,23 @@ export default function AdminVehicleRequestsPage() { } }; + // Regenerate PDF for a car + const regeneratePdf = async (carId: number) => { + try { + setRegeneratingPdfId(carId); + await adminPdfApi.regenerateSingle(carId); + // Reload request detail to refresh PDF status + if (selectedRequest) { + await loadRequestDetail(selectedRequest.request.id); + } + } catch (error) { + console.error('Failed to regenerate PDF:', error); + alert('Failed to regenerate PDF. Please try again.'); + } finally { + setRegeneratingPdfId(null); + } + }; + // Format date const formatDate = (dateString: string) => { return new Date(dateString).toLocaleDateString('ko-KR', { @@ -304,42 +322,89 @@ export default function AdminVehicleRequestsPage() { ) : (
- {selectedRequest.approved_vehicles.map((vehicle) => ( -
- {vehicle.car_data.main_image && ( - - )} -
-

{vehicle.car_data.car_name}

-

- {vehicle.car_data.year} | {vehicle.car_data.mileage?.toLocaleString()}km -

+ {selectedRequest.approved_vehicles.map((vehicle) => { + const hasPdf = vehicle.car_data.has_pdf === true; + const isSoldout = vehicle.car_data.soldout === true; + const checkNum = vehicle.car_data.check_num; + + return ( +
+ {vehicle.car_data.main_image && ( +
+ + {isSoldout && ( + SOLD + )} +
+ )} +
+

{vehicle.car_data.car_name}

+

+ {vehicle.car_data.year} | {vehicle.car_data.mileage?.toLocaleString()}km +

+ {/* PDF Status */} +
+ {hasPdf ? ( + + + + + PDF OK + + ) : ( + + + + + No PDF + {vehicle.car_id && checkNum && ( + + )} + {!checkNum && (no check_num)} + + )} +
+
+
+

+ {vehicle.car_data.final_price?.toLocaleString()}만원 +

+ +
-
-

- {vehicle.car_data.final_price?.toLocaleString()}만원 -

- -
-
- ))} + ); + })}
)}
diff --git a/frontend/src/app/profile/page.tsx b/frontend/src/app/profile/page.tsx index 61ac44f..f3d2561 100644 --- a/frontend/src/app/profile/page.tsx +++ b/frontend/src/app/profile/page.tsx @@ -297,21 +297,13 @@ export default function ProfilePage() {

- {/* Danger Zone - Account Withdrawal */} -
-

- {language === 'ko' ? '계정 탈퇴' : 'Account Withdrawal'} -

-

- {language === 'ko' - ? '계정을 탈퇴하면 모든 데이터가 삭제됩니다. 이 작업은 되돌릴 수 없습니다.' - : 'Once you withdraw your account, all your data will be deleted. This action cannot be undone.'} -

+ {/* Account Withdrawal - Subtle link */} +