Add staging environment documentation and minor fixes

- 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 <noreply@anthropic.com>
This commit is contained in:
AutonetSellCar Deploy
2026-01-03 16:22:34 +09:00
parent 6ba254bfeb
commit 718c5b0474
5 changed files with 163 additions and 51 deletions

View File

@@ -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<VehicleRequest[]>([]);
@@ -10,6 +10,7 @@ export default function AdminVehicleRequestsPage() {
const [isLoading, setIsLoading] = useState(true);
const [statusFilter, setStatusFilter] = useState<string>('');
const [deletingVehicleId, setDeletingVehicleId] = useState<number | null>(null);
const [regeneratingPdfId, setRegeneratingPdfId] = useState<number | null>(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() {
</div>
) : (
<div className="space-y-2 max-h-[300px] overflow-y-auto">
{selectedRequest.approved_vehicles.map((vehicle) => (
<div key={vehicle.id} className="bg-gray-50 rounded-lg p-3 flex items-center gap-3">
{vehicle.car_data.main_image && (
<img
src={vehicle.car_data.main_image}
alt=""
className="w-16 h-12 object-cover rounded"
/>
)}
<div className="flex-1 min-w-0">
<p className="font-medium text-sm truncate">{vehicle.car_data.car_name}</p>
<p className="text-xs text-gray-500">
{vehicle.car_data.year} | {vehicle.car_data.mileage?.toLocaleString()}km
</p>
{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 (
<div key={vehicle.id} className={`bg-gray-50 rounded-lg p-3 flex items-center gap-3 ${isSoldout ? 'opacity-60' : ''}`}>
{vehicle.car_data.main_image && (
<div className="relative">
<img
src={vehicle.car_data.main_image}
alt=""
className={`w-16 h-12 object-cover rounded ${isSoldout ? 'grayscale' : ''}`}
/>
{isSoldout && (
<span className="absolute top-0 left-0 bg-red-500 text-white text-[8px] px-1 rounded">SOLD</span>
)}
</div>
)}
<div className="flex-1 min-w-0">
<p className="font-medium text-sm truncate">{vehicle.car_data.car_name}</p>
<p className="text-xs text-gray-500">
{vehicle.car_data.year} | {vehicle.car_data.mileage?.toLocaleString()}km
</p>
{/* PDF Status */}
<div className="flex items-center gap-1 mt-1">
{hasPdf ? (
<span className="text-xs text-green-600 flex items-center gap-1">
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
PDF OK
</span>
) : (
<span className="text-xs text-red-500 flex items-center gap-1">
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
No PDF
{vehicle.car_id && checkNum && (
<button
onClick={() => 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 ? (
<span className="flex items-center gap-1">
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-blue-500"></div>
Generating...
</span>
) : (
'Retry'
)}
</button>
)}
{!checkNum && <span className="text-gray-400">(no check_num)</span>}
</span>
)}
</div>
</div>
<div className="text-right flex items-center gap-2">
<p className="text-sm font-medium text-primary-600">
{vehicle.car_data.final_price?.toLocaleString()}
</p>
<button
onClick={() => 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 ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-red-500"></div>
) : (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
)}
</button>
</div>
</div>
<div className="text-right flex items-center gap-2">
<p className="text-sm font-medium text-primary-600">
{vehicle.car_data.final_price?.toLocaleString()}
</p>
<button
onClick={() => 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 ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-red-500"></div>
) : (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
)}
</button>
</div>
</div>
))}
);
})}
</div>
)}
</div>

View File

@@ -297,21 +297,13 @@ export default function ProfilePage() {
</p>
</div>
{/* Danger Zone - Account Withdrawal */}
<div className="mt-8 bg-white rounded-xl shadow-lg p-6 border border-red-100">
<h2 className="text-xl font-semibold text-red-600 mb-4">
{language === 'ko' ? '계정 탈퇴' : 'Account Withdrawal'}
</h2>
<p className="text-gray-600 text-sm mb-4">
{language === 'ko'
? '계정을 탈퇴하면 모든 데이터가 삭제됩니다. 이 작업은 되돌릴 수 없습니다.'
: 'Once you withdraw your account, all your data will be deleted. This action cannot be undone.'}
</p>
{/* Account Withdrawal - Subtle link */}
<div className="mt-8 pt-4 border-t border-gray-100 text-center">
<button
onClick={() => 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'}
</button>
</div>
</div>