From 718c5b0474c798d59232eef78946e2842d2b6f46 Mon Sep 17 00:00:00 2001
From: AutonetSellCar Deploy
Date: Sat, 3 Jan 2026 16:22:34 +0900
Subject: [PATCH] Add staging environment documentation and minor fixes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Document staging vs production DB separation (autonet_staging)
- Add staging sync and deployment commands to CLAUDE.md
- Update changelog with 2025-01-03 changes
๐ค Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5
---
CLAUDE.md | 46 ++++++
backend/app/api/carmodoo.py | 11 +-
frontend/package.json | 4 +-
.../src/app/admin/vehicle-requests/page.tsx | 137 +++++++++++++-----
frontend/src/app/profile/page.tsx | 16 +-
5 files changed, 163 insertions(+), 51 deletions(-)
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 && (
+ regeneratePdf(vehicle.car_id!)}
+ disabled={regeneratingPdfId === vehicle.car_id}
+ className="ml-1 text-blue-500 hover:text-blue-700 underline disabled:opacity-50"
+ title="Retry PDF generation"
+ >
+ {regeneratingPdfId === vehicle.car_id ? (
+
+
+ Generating...
+
+ ) : (
+ 'Retry'
+ )}
+
+ )}
+ {!checkNum && (no check_num) }
+
+ )}
+
+
+
+
+ {vehicle.car_data.final_price?.toLocaleString()}๋ง์
+
+
deleteVehicleFromRequest(vehicle.id)}
+ disabled={deletingVehicleId === vehicle.id}
+ className="text-red-500 hover:text-red-700 p-1 disabled:opacity-50"
+ title="Remove from list"
+ >
+ {deletingVehicleId === vehicle.id ? (
+
+ ) : (
+
+
+
+ )}
+
+
-
-
- {vehicle.car_data.final_price?.toLocaleString()}๋ง์
-
-
deleteVehicleFromRequest(vehicle.id)}
- disabled={deletingVehicleId === vehicle.id}
- className="text-red-500 hover:text-red-700 p-1 disabled:opacity-50"
- title="Remove from list"
- >
- {deletingVehicleId === vehicle.id ? (
-
- ) : (
-
-
-
- )}
-
-
-
- ))}
+ );
+ })}
)}
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 */}
+
setShowWithdrawModal(true)}
- className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition"
+ className="text-xs text-gray-400 hover:text-gray-500 transition"
>
- {language === 'ko' ? '๊ณ์ ํํด ์์ฒญ' : 'Request Account Withdrawal'}
+ {language === 'ko' ? '๊ณ์ ํํด' : 'Delete Account'}