Add SOLD OUT badge and improve deployment docs

- Add SOLD OUT overlay on car detail page image
- Add SOLD OUT badge next to car name
- Add Sold Out status in Admin Cars detail view
- Add soldout field to Car TypeScript interface
- Create PRODUCTION_VALUES.md for deployment reference
- Update CLAUDE.md with CRITICAL deployment section
- Update TROUBLESHOOTING.md with recurring errors

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
AutonetSellCar Deploy
2026-01-03 09:05:16 +09:00
parent 2da464694b
commit b1afea79d9
8 changed files with 593 additions and 25 deletions

View File

@@ -8,6 +8,59 @@
---
# ⛔ CRITICAL: 배포 전 필수 확인사항 (DO NOT SKIP!)
## 🚨 Production 환경 고정값 (절대 변경 금지!)
| 항목 | 올바른 값 | 잘못된 예 |
|------|----------|----------|
| **DB_NAME** | `autonet` | ~~mongolcar~~, ~~autonet_db~~ |
| **DB_PASSWORD** | `roskfl@1122` (@ 포함, URL 인코딩 필요) | - |
| **API_URL (Frontend)** | `https://autonetsellcar.com` | ~~http://192.168.0.202:8000~~ |
| **Uploads 경로** | `/opt/autonet/production/backend/uploads` | ~~`/home/damon/mongolcar/data/uploads`~~ |
| **배포 디렉토리** | `/opt/autonet/production` | ~~`/home/damon/mongolcar`~~ |
## 🚫 절대 하지 말 것
1. **수동 `docker run`에서 환경변수 직접 타이핑 금지** - [PRODUCTION_VALUES.md](./PRODUCTION_VALUES.md)에서 복사만 허용
2. **DB_NAME 직접 타이핑 금지** - 반드시 `autonet` 복사/붙여넣기
3. **`.env` 파일 덮어쓰기 금지** - 서버의 기존 `.env` 보존
4. **`/home/damon/mongolcar/data/uploads` 사용 금지** - 비어있는 잘못된 경로
## ✅ 배포 전 필수 검증 명령어
```bash
# 1. DB 이름 확인 (반드시 autonet이어야 함!)
ssh server1 "docker exec postgres-primary psql -U admin -d autonet -c 'SELECT COUNT(*) FROM cars;'"
# 2. Uploads 경로에 파일 있는지 확인
ssh server2 "ls /opt/autonet/production/backend/uploads/cars/ | head -5"
# 3. 배포 후 API 확인
curl -s https://autonetsellcar.com/api/hero-banners/ | head -c 100
```
## 📋 표준 컨테이너 실행 명령어 (복사해서 사용!)
**⚠️ 아래 명령어를 그대로 복사해서 사용하세요. 절대 직접 타이핑하지 마세요!**
### Backend 실행
```bash
ssh server2 "docker stop autonet-backend 2>/dev/null; docker rm autonet-backend 2>/dev/null; docker run -d --name autonet-backend --restart unless-stopped -p 8000:8000 -e USE_SQLITE=False -e DB_HOST=192.168.0.201 -e DB_PORT=5432 -e DB_NAME=autonet -e DB_USER=admin -e 'DB_PASSWORD=roskfl@1122' -e REDIS_HOST=192.168.0.201 -e REDIS_PORT=6379 -e 'REDIS_PASSWORD=roskfl@1122' -e SECRET_KEY=YourSuperSecretKeyForJWT123! -e AGENT_API_KEY=AgentApiKey123! -v /opt/autonet/production/backend/uploads:/app/uploads --network mongolcar-network production-backend"
```
### Frontend 빌드 전 (.env.production 설정)
```bash
ssh server2 "echo 'NEXT_PUBLIC_API_URL=https://autonetsellcar.com' > /home/damon/mongolcar/frontend/.env.production"
```
### Frontend 실행
```bash
ssh server2 "docker stop autonet-frontend 2>/dev/null; docker rm autonet-frontend 2>/dev/null; docker run -d --name autonet-frontend --restart unless-stopped -p 3000:3000 --network mongolcar-network production-frontend"
```
---
## 1. 프로젝트 구조
```

View File

@@ -2,6 +2,9 @@
이 문서는 시스템 아키텍처와 코드 수정부터 운영 배포까지의 전체 과정을 설명합니다.
> **중요**: 배포 전 반드시 [PRODUCTION_VALUES.md](./PRODUCTION_VALUES.md)의 값을 확인하세요!
> 반복되는 오류 해결은 [TROUBLESHOOTING.md](./TROUBLESHOOTING.md)를 참고하세요.
---
## 1. 시스템 아키텍처 개요
@@ -150,7 +153,7 @@ docker exec postgres-primary env | grep POSTGRES
# 출력:
# POSTGRES_USER=admin
# POSTGRES_PASSWORD=roskfl@1122
# POSTGRES_DB=mongolcar
# POSTGRES_DB=mongolcar (컨테이너 기본 DB, AutonetSellCar는 autonet DB 사용!)
```
### 4.2 데이터베이스 생성

179
PRODUCTION_VALUES.md Normal file
View File

@@ -0,0 +1,179 @@
# AutonetSellCar Production Values (Single Source of Truth)
**이 파일의 값들은 절대 변경하지 마세요!**
**DO NOT MODIFY THESE VALUES!**
---
## Server Information
| 서버 | IP | 역할 |
|------|-----|------|
| Server1 | 192.168.0.201 | PostgreSQL Primary |
| Server2 | 192.168.0.202 | Production (Frontend + Backend) |
| Server3 | 192.168.0.203 | Grantech.kr |
| Server4 | 192.168.0.204 | Development |
---
## Database Configuration
| 항목 | 값 | 비고 |
|------|-----|------|
| **DB_NAME** | `autonet` | ~~mongolcar~~ 절대 아님! |
| **DB_USER** | `admin` | |
| **DB_PASSWORD** | `roskfl@1122` | `@` 포함, URL 인코딩 필요 |
| **DB_HOST** | `192.168.0.201` | Server1 |
| **DB_PORT** | `5432` | PostgreSQL 기본 포트 |
### Database URL Format
```
postgresql://admin:roskfl%401122@192.168.0.201:5432/autonet
^^^^^^^^
@가 %40으로 인코딩됨
```
### config.py에서 quote_plus() 필수
```python
from urllib.parse import quote_plus
encoded_password = quote_plus(self.DB_PASSWORD) # roskfl@1122 → roskfl%401122
```
---
## Docker Container Configuration
### Backend Container
```bash
docker run -d \
--name autonet-backend \
--restart unless-stopped \
-p 8000:8000 \
-e USE_SQLITE=False \
-e DB_HOST=192.168.0.201 \
-e DB_PORT=5432 \
-e DB_NAME=autonet \
-e DB_USER=admin \
-e "DB_PASSWORD=roskfl@1122" \
-v /opt/autonet/production/backend/uploads:/app/uploads \
production-backend
```
**주의사항:**
- `DB_NAME=autonet` (절대 mongolcar 아님!)
- `-v /opt/autonet/production/backend/uploads:/app/uploads` (절대 `/home/damon/mongolcar/...` 아님!)
- `DB_PASSWORD``@` 포함됨 - 따옴표로 감싸기
### Frontend Container
```bash
# 빌드 전 .env.production 확인!
echo 'NEXT_PUBLIC_API_URL=https://autonetsellcar.com' > /opt/autonet/production/frontend/.env.production
# 빌드 (반드시 --no-cache)
docker build --no-cache -t production-frontend .
# 실행
docker run -d \
--name autonet-frontend \
--restart unless-stopped \
-p 3000:3000 \
production-frontend
```
**주의사항:**
- `NEXT_PUBLIC_API_URL=https://autonetsellcar.com` (절대 `http://192.168.0.202:8000` 아님!)
- HTTPS 필수 (Mixed Content 방지)
---
## File Paths
| 용도 | 올바른 경로 | 잘못된 경로 |
|------|-------------|-------------|
| **Uploads** | `/opt/autonet/production/backend/uploads` | `/home/damon/mongolcar/data/uploads` |
| **Backend 소스** | `/opt/autonet/production/backend` | - |
| **Frontend 소스** | `/opt/autonet/production/frontend` | - |
---
## Environment Variables
### Backend (.env)
```env
USE_SQLITE=False
DB_HOST=192.168.0.201
DB_PORT=5432
DB_NAME=autonet
DB_USER=admin
DB_PASSWORD=roskfl@1122
```
### Frontend (.env.production)
```env
NEXT_PUBLIC_API_URL=https://autonetsellcar.com
```
---
## Verification Commands
### 1. DB 연결 확인
```bash
ssh server2 "docker exec autonet-backend python -c \"from app.config import get_settings; print('DB_NAME:', get_settings().DB_NAME)\""
# 예상 출력: DB_NAME: autonet
```
### 2. API 데이터 확인
```bash
curl -s https://autonetsellcar.com/api/hero-banners/ | jq 'length'
# 예상 출력: 8 이상 (0이면 문제!)
```
### 3. 이미지 확인
```bash
curl -I https://autonetsellcar.com/uploads/cars/1/image_0.jpg
# 예상 출력: HTTP/2 200
```
### 4. Frontend API URL 확인
```bash
ssh server2 "docker exec autonet-frontend grep -o 'autonetsellcar.com' /app/.next/static/chunks/app/**/*.js | head -1"
# 예상 출력: autonetsellcar.com (localhost나 192.168.0.202가 아님!)
```
---
## Quick Reference Card
```
┌─────────────────────────────────────────────────────────────┐
│ PRODUCTION VALUES │
├─────────────────────────────────────────────────────────────┤
│ DB_NAME: autonet (NOT mongolcar!) │
│ DB_PASSWORD: roskfl@1122 (@ needs URL encoding) │
│ API_URL: https://autonetsellcar.com (NOT http://...) │
│ UPLOADS: /opt/autonet/production/backend/uploads │
└─────────────────────────────────────────────────────────────┘
```
---
## Change History
| 날짜 | 변경 내용 |
|------|----------|
| 2026-01-02 | 문서 최초 생성 (반복 배포 오류 방지 목적) |
---
**이 문서의 값과 다르면 무조건 틀린 것입니다!**

View File

@@ -233,9 +233,162 @@ docker exec autonet-frontend sh -c "grep -r 'localhost:8000' /app/.next/static/c
---
---
## 2026-01-02: 반복되는 배포 오류 종합 (CRITICAL)
### 문제 1: DB_NAME 오류 (`mongolcar` vs `autonet`)
**증상**:
- 사이트에 차량/배너 없음
- API가 빈 배열 `[]` 반환
- 로그인은 되지만 데이터 없음
**원인**:
- 수동 `docker run` 시 DB_NAME을 `mongolcar`로 잘못 입력
- PostgreSQL에 `mongolcar``autonet` 두 DB가 존재
- `mongolcar`는 비어있고, 실제 데이터는 `autonet`에 있음
**확인 방법**:
```bash
# 현재 연결된 DB 확인
ssh server2 "docker exec autonet-backend python -c \"from app.config import get_settings; print('DB_NAME:', get_settings().DB_NAME)\""
# DB별 데이터 확인
ssh server1 "docker exec postgres-primary psql -U admin -d autonet -c 'SELECT COUNT(*) FROM cars;'"
ssh server1 "docker exec postgres-primary psql -U admin -d mongolcar -c 'SELECT COUNT(*) FROM cars;'"
```
**해결**:
- `DB_NAME=autonet` 확인 후 컨테이너 재시작
- CLAUDE.md의 표준 명령어 복사해서 사용
---
### 문제 2: 비밀번호 `@` 파싱 오류
**증상**:
- `could not translate host name "1122@192.168.0.201"`
- 백엔드 컨테이너 계속 재시작
**원인**:
- `DB_PASSWORD=roskfl@1122``@`가 URL에서 호스트 구분자로 인식됨
- `config.py``quote_plus()` URL 인코딩이 없거나, Docker 이미지가 오래됨
**확인 방법**:
```bash
# config.py에 quote_plus 있는지 확인
ssh server2 "docker exec autonet-backend cat /app/app/config.py | grep quote_plus"
```
**해결**:
1. `config.py``quote_plus()` 적용 확인
2. Docker 이미지 재빌드: `docker build --no-cache -t production-backend ./backend`
---
### 문제 3: 이미지 404 (잘못된 uploads 경로)
**증상**:
- `/uploads/cars/40/image_0.jpg` → 404
- Hero 배너에 기본 이미지만 표시
**원인**:
- Production 컨테이너가 잘못된 경로 마운트: `/home/damon/mongolcar/data/uploads` (비어있음!)
- 실제 이미지는 `/opt/autonet/production/backend/uploads`에 있음
**확인 방법**:
```bash
# 올바른 경로 (파일 있어야 함)
ssh server2 "ls /opt/autonet/production/backend/uploads/cars/ | head -5"
# 잘못된 경로 (비어있음!)
ssh server2 "ls /home/damon/mongolcar/data/uploads/cars/ 2>/dev/null || echo 'Empty or not exists'"
```
**해결**:
- 볼륨 마운트를 `-v /opt/autonet/production/backend/uploads:/app/uploads`로 변경
- CLAUDE.md의 표준 명령어 사용
---
### 문제 4: Mixed Content (HTTPS/HTTP)
**증상**:
- 브라우저 콘솔에 Mixed Content 오류
- API 요청 실패
- 사이트에 기본 이미지만 표시
**원인**:
- Frontend가 `NEXT_PUBLIC_API_URL=http://192.168.0.202:8000`으로 빌드됨
- HTTPS 페이지에서 HTTP API 호출 시 브라우저가 차단
**확인 방법**:
```bash
# 빌드된 JS에서 API URL 확인
ssh server2 "docker exec autonet-frontend grep -r '192.168.0.202:8000' /app/.next/static/chunks/ | head -1"
```
**해결**:
```bash
# Frontend 빌드 전 필수!
ssh server2 "echo 'NEXT_PUBLIC_API_URL=https://autonetsellcar.com' > /home/damon/mongolcar/frontend/.env.production"
ssh server2 "cd /home/damon/mongolcar/frontend && docker build --no-cache -t production-frontend ."
```
---
### 문제 5: .env 파일이 디렉토리로 생성됨
**증상**:
- 로그인 실패
- `(sqlite3.OperationalError) no such column` 에러
- 환경변수 로드 실패
**원인**:
- docker-compose 볼륨 마운트 시 `.env` 파일이 없으면 디렉토리로 생성됨
**확인 방법**:
```bash
ssh server2 "ls -la /opt/autonet/production/backend/.env"
# 'd'로 시작하면 디렉토리 (문제!)
```
**해결**:
```bash
# 디렉토리면 삭제 후 파일로 재생성
ssh server2 "rm -rf /opt/autonet/production/backend/.env"
ssh server2 "cat > /opt/autonet/production/backend/.env << 'EOF'
USE_SQLITE=False
DB_HOST=192.168.0.201
DB_PORT=5432
DB_NAME=autonet
DB_USER=admin
DB_PASSWORD=roskfl@1122
EOF"
```
---
## 재발 방지 체크리스트
배포 시 반드시 확인:
- [ ] DB_NAME이 `autonet`인가? (절대 `mongolcar` 아님!)
- [ ] Uploads 경로가 `/opt/autonet/production/backend/uploads`인가?
- [ ] Frontend `.env.production``https://autonetsellcar.com`이 설정되어 있는가?
- [ ] `--no-cache`로 이미지를 빌드했는가?
- [ ] 배포 후 `curl https://autonetsellcar.com/api/hero-banners/`로 데이터 확인했는가?
---
## 변경 이력
| 날짜 | 문제 | 해결 |
|------|------|------|
| 2026-01-02 | DB_NAME 오류 (mongolcar vs autonet) | DB_NAME=autonet 확인, 표준 명령어 사용 |
| 2026-01-02 | 비밀번호 @ 파싱 오류 | quote_plus() URL 인코딩 적용 |
| 2026-01-02 | 이미지 404 (잘못된 uploads 경로) | /opt/autonet/production/backend/uploads 사용 |
| 2026-01-02 | Mixed Content 오류 | NEXT_PUBLIC_API_URL=https://autonetsellcar.com |
| 2024-12-30 | 운영서버 이미지 미표시 (배너) | 소스 코드 동기화 + Docker 재빌드 |
| 2024-12-30 | 차량 상세 이미지 미표시 | `getImageUrl()` 함수 수정 |

View File

@@ -4,7 +4,7 @@ from typing import List
from datetime import datetime, timedelta
from ..database import get_db
from ..models import VehicleRequest, RequestVehicle, PurchasedVehicle, User, DealerInfo, SystemSettings
from ..models import VehicleRequest, RequestVehicle, PurchasedVehicle, User, DealerInfo, SystemSettings, Car
from ..schemas import (
VehicleRequestCreate, VehicleRequestResponse,
RequestVehicleCreate, RequestVehicleResponse, RequestVehicleApprove,
@@ -100,9 +100,21 @@ def get_my_requests(
else:
approved_vehicles = []
# Enrich approved vehicles with latest soldout status from cars table
enriched_vehicles = []
for v in approved_vehicles:
vehicle_response = RequestVehicleResponse.model_validate(v)
# Get latest soldout status from cars table
if v.car_id:
car = db.query(Car).filter(Car.id == v.car_id).first()
if car:
# Add soldout status to car_data
vehicle_response.car_data = {**vehicle_response.car_data, "soldout": car.soldout}
enriched_vehicles.append(vehicle_response)
result.append(VehicleRequestWithVehicles(
request=VehicleRequestResponse.model_validate(req),
approved_vehicles=[RequestVehicleResponse.model_validate(v) for v in approved_vehicles]
approved_vehicles=enriched_vehicles
))
return result
@@ -189,9 +201,30 @@ def admin_get_request_detail(
if not request:
raise HTTPException(status_code=404, detail="Request not found")
# Import CarPerformanceCheck for PDF status
from ..models import CarPerformanceCheck
# Enrich with PDF status and soldout
enriched_vehicles = []
for v in request.recommended_vehicles:
vehicle_response = RequestVehicleResponse.model_validate(v)
# Get PDF status and soldout from car
if v.car_id:
car = db.query(Car).filter(Car.id == v.car_id).first()
perf_check = db.query(CarPerformanceCheck).filter(CarPerformanceCheck.car_id == v.car_id).first()
vehicle_response.car_data = {
**vehicle_response.car_data,
"soldout": car.soldout if car else False,
"has_pdf": bool(perf_check and perf_check.pdf_path),
"check_num": perf_check.check_number if perf_check else None,
}
enriched_vehicles.append(vehicle_response)
return VehicleRequestWithVehicles(
request=VehicleRequestResponse.model_validate(request),
approved_vehicles=[RequestVehicleResponse.model_validate(v) for v in request.recommended_vehicles]
approved_vehicles=enriched_vehicles
)
@@ -202,7 +235,7 @@ def admin_add_vehicle(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Admin: Add a vehicle to a request"""
"""Admin: Add a vehicle to a request (also imports to cars table)"""
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Admin access required")
@@ -210,9 +243,51 @@ def admin_add_vehicle(
if not request:
raise HTTPException(status_code=404, detail="Request not found")
# Extract car data
car_data = vehicle_data.car_data
source_id = str(car_data.get("id", ""))
# Check if car already exists in cars table
existing_car = None
if source_id:
existing_car = db.query(Car).filter(
Car.source == "carmodoo",
Car.source_id == source_id
).first()
car_id = None
if existing_car:
car_id = existing_car.id
elif source_id:
# Create new car record from car_data
new_car = Car(
source="carmodoo",
source_id=source_id,
car_name=car_data.get("car_name", ""),
year=car_data.get("year"),
mileage=car_data.get("mileage"),
price_krw=car_data.get("original_price"),
fuel=car_data.get("fuel"),
transmission=car_data.get("transmission"),
color=car_data.get("color"),
displacement=car_data.get("displacement"),
margin_krw=car_data.get("korea_margin"),
margin_mn=car_data.get("mongolia_margin"),
check_num=car_data.get("check_num"),
is_displayed=True, # Displayed so user can view recommended car
status="active"
)
db.add(new_car)
db.flush()
car_id = new_car.id
# Update car_data with local car_id for frontend
car_data["local_car_id"] = car_id
vehicle = RequestVehicle(
request_id=request_id,
car_data=vehicle_data.car_data,
car_id=car_id,
car_data=car_data,
is_approved=vehicle_data.is_approved,
approved_at=datetime.utcnow() if vehicle_data.is_approved else None
)

View File

@@ -218,6 +218,17 @@ export default function CarsAdminPage() {
added: number;
errors: number;
} | null>(null);
const [requestInfo, setRequestInfo] = useState<{
user_email?: string;
user_name?: string;
maker_name?: string;
model_name?: string;
grade_name?: string;
year_from?: number;
year_to?: number;
mileage_max?: number;
fuel?: string;
} | null>(null);
// Dealer description editing state
const [showDescEditModal, setShowDescEditModal] = useState(false);
@@ -299,6 +310,21 @@ export default function CarsAdminPage() {
// Switch to Carmodoo tab
setActiveTab('carmodoo');
// Fetch request details for display
vehicleRequestsApi.adminGetRequestDetail(parseInt(requestId)).then(data => {
setRequestInfo({
user_email: data.request.user_email,
user_name: data.request.user_name,
maker_name: data.request.maker_name,
model_name: data.request.model_name,
grade_name: data.request.grade_name,
year_from: data.request.year_from,
year_to: data.request.year_to,
mileage_max: data.request.mileage_max,
fuel: data.request.fuel,
});
}).catch(err => console.error('Failed to load request info:', err));
}
}, [requestId]);
@@ -1195,24 +1221,76 @@ export default function CarsAdminPage() {
{/* Request Mode Banner */}
{requestId && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="bg-blue-100 rounded-full p-2">
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<p className="font-medium text-blue-800">Adding vehicles to Request #{requestId}</p>
<p className="text-sm text-blue-600">Search and select vehicles, then click "Add to Request" button.</p>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<div className="bg-blue-100 rounded-full p-2">
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<p className="font-medium text-blue-800">
Adding vehicles to Request #{requestId}
{requestInfo?.user_email && (
<span className="ml-2 text-sm font-normal text-blue-600">
({requestInfo.user_email})
</span>
)}
</p>
</div>
</div>
<button
onClick={() => router.push('/admin/vehicle-requests')}
className="text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1"
>
Back to Requests
</button>
</div>
<button
onClick={() => router.push('/admin/vehicle-requests')}
className="text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1"
>
Back to Requests
</button>
{requestInfo && (
<div className="bg-white/50 rounded-lg p-3 grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
{requestInfo.maker_name && (
<div>
<span className="text-blue-500">Maker:</span>
<span className="ml-1 font-medium text-blue-800">{requestInfo.maker_name}</span>
</div>
)}
{requestInfo.model_name && (
<div>
<span className="text-blue-500">Model:</span>
<span className="ml-1 font-medium text-blue-800">{requestInfo.model_name}</span>
</div>
)}
{requestInfo.grade_name && (
<div>
<span className="text-blue-500">Grade:</span>
<span className="ml-1 font-medium text-blue-800">{requestInfo.grade_name}</span>
</div>
)}
{(requestInfo.year_from || requestInfo.year_to) && (
<div>
<span className="text-blue-500">Year:</span>
<span className="ml-1 font-medium text-blue-800">
{requestInfo.year_from || '-'} ~ {requestInfo.year_to || '-'}
</span>
</div>
)}
{requestInfo.mileage_max && (
<div>
<span className="text-blue-500">Mileage:</span>
<span className="ml-1 font-medium text-blue-800">
~{Math.round(requestInfo.mileage_max / 10000)}km
</span>
</div>
)}
{requestInfo.fuel && (
<div>
<span className="text-blue-500">Fuel:</span>
<span className="ml-1 font-medium text-blue-800">{requestInfo.fuel}</span>
</div>
)}
</div>
)}
</div>
)}
@@ -2568,6 +2646,16 @@ export default function CarsAdminPage() {
{selectedCar.status}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Sold Out</span>
<span className={`px-2 py-1 rounded text-xs font-medium ${
selectedCar.soldout
? 'bg-red-100 text-red-700'
: 'bg-green-100 text-green-700'
}`}>
{selectedCar.soldout ? 'SOLD OUT' : 'Available'}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Seize Count</span>
<span className={`font-medium ${(selectedCar.seize_count || 0) > 0 ? 'text-red-600' : ''}`}>

View File

@@ -410,6 +410,15 @@ export default function CarDetailPage() {
</p>
</div>
)}
{/* Soldout overlay */}
{car.soldout && (
<div className="absolute inset-0 flex items-center justify-center bg-black/40">
<span className="bg-red-600 text-white px-6 py-3 rounded-lg font-bold text-2xl transform -rotate-12 shadow-lg">
{language === 'ko' ? '판매완료' : 'SOLD OUT'}
</span>
</div>
)}
</div>
{/* Thumbnails */}
@@ -527,9 +536,16 @@ export default function CarDetailPage() {
{/* Details */}
<div>
<h1 className="text-3xl font-bold text-gray-800 mb-2">
{translate(car.car_name) || `${translate(car.maker?.name) || ''} ${translate(car.model?.name) || ''}`.trim() || '-'}
</h1>
<div className="flex items-center gap-3 mb-2">
<h1 className="text-3xl font-bold text-gray-800">
{translate(car.car_name) || `${translate(car.maker?.name) || ''} ${translate(car.model?.name) || ''}`.trim() || '-'}
</h1>
{car.soldout && (
<span className="px-3 py-1 bg-red-600 text-white text-sm font-bold rounded-full">
{language === 'ko' ? '판매완료' : 'SOLD OUT'}
</span>
)}
</div>
{(() => {
const price = formatPrice(car.final_price_krw || car.price_krw);

View File

@@ -48,6 +48,7 @@ export interface Car {
dealer_description_mn?: string;
dealer_description_ru?: string;
status: string;
soldout?: boolean;
created_at: string;
updated_at: string;
maker?: CarMaker;