diff --git a/CLAUDE.md b/CLAUDE.md index bde5b6e..ff97d12 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. 프로젝트 구조 ``` diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md index 8ebf872..431d5b6 100644 --- a/DEPLOYMENT_GUIDE.md +++ b/DEPLOYMENT_GUIDE.md @@ -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 데이터베이스 생성 diff --git a/PRODUCTION_VALUES.md b/PRODUCTION_VALUES.md new file mode 100644 index 0000000..75d9b66 --- /dev/null +++ b/PRODUCTION_VALUES.md @@ -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 | 문서 최초 생성 (반복 배포 오류 방지 목적) | + +--- + +**이 문서의 값과 다르면 무조건 틀린 것입니다!** diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 4edee9a..aac7018 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -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()` 함수 수정 | diff --git a/backend/app/api/vehicle_requests.py b/backend/app/api/vehicle_requests.py index 337bf76..ba580a7 100644 --- a/backend/app/api/vehicle_requests.py +++ b/backend/app/api/vehicle_requests.py @@ -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 ) diff --git a/frontend/src/app/admin/cars/page.tsx b/frontend/src/app/admin/cars/page.tsx index d72a270..5b015cc 100644 --- a/frontend/src/app/admin/cars/page.tsx +++ b/frontend/src/app/admin/cars/page.tsx @@ -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 && ( -
-
-
- - - -
-
-

Adding vehicles to Request #{requestId}

-

Search and select vehicles, then click "Add to Request" button.

+
+
+
+
+ + + +
+
+

+ Adding vehicles to Request #{requestId} + {requestInfo?.user_email && ( + + ({requestInfo.user_email}) + + )} +

+
+
- + {requestInfo && ( +
+ {requestInfo.maker_name && ( +
+ Maker: + {requestInfo.maker_name} +
+ )} + {requestInfo.model_name && ( +
+ Model: + {requestInfo.model_name} +
+ )} + {requestInfo.grade_name && ( +
+ Grade: + {requestInfo.grade_name} +
+ )} + {(requestInfo.year_from || requestInfo.year_to) && ( +
+ Year: + + {requestInfo.year_from || '-'} ~ {requestInfo.year_to || '-'} + +
+ )} + {requestInfo.mileage_max && ( +
+ Mileage: + + ~{Math.round(requestInfo.mileage_max / 10000)}만km + +
+ )} + {requestInfo.fuel && ( +
+ Fuel: + {requestInfo.fuel} +
+ )} +
+ )}
)} @@ -2568,6 +2646,16 @@ export default function CarsAdminPage() { {selectedCar.status}
+
+ Sold Out + + {selectedCar.soldout ? 'SOLD OUT' : 'Available'} + +
Seize Count 0 ? 'text-red-600' : ''}`}> diff --git a/frontend/src/app/cars/[id]/page.tsx b/frontend/src/app/cars/[id]/page.tsx index 6ea927f..b7d7ccd 100644 --- a/frontend/src/app/cars/[id]/page.tsx +++ b/frontend/src/app/cars/[id]/page.tsx @@ -410,6 +410,15 @@ export default function CarDetailPage() {

)} + + {/* Soldout overlay */} + {car.soldout && ( +
+ + {language === 'ko' ? '판매완료' : 'SOLD OUT'} + +
+ )}
{/* Thumbnails */} @@ -527,9 +536,16 @@ export default function CarDetailPage() { {/* Details */}
-

- {translate(car.car_name) || `${translate(car.maker?.name) || ''} ${translate(car.model?.name) || ''}`.trim() || '-'} -

+
+

+ {translate(car.car_name) || `${translate(car.maker?.name) || ''} ${translate(car.model?.name) || ''}`.trim() || '-'} +

+ {car.soldout && ( + + {language === 'ko' ? '판매완료' : 'SOLD OUT'} + + )} +
{(() => { const price = formatPrice(car.final_price_krw || car.price_krw); diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index f2306be..514ea5c 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -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;