Add deployment scripts and troubleshooting docs
- deploy_ru_server2.sh: Russian language deployment script for server2 - update_db_ru.py: Database migration for Russian columns - TROUBLESHOOTING.md: Common issues and solutions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
241
TROUBLESHOOTING.md
Normal file
241
TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# AutonetSellCar 트러블슈팅 가이드
|
||||
|
||||
이 문서는 배포 및 운영 중 발생한 문제와 해결 방법을 정리합니다.
|
||||
|
||||
---
|
||||
|
||||
## 2024-12-30: 운영서버 이미지 표시 문제
|
||||
|
||||
### 증상
|
||||
- 개발서버(192.168.0.204)에서는 이미지가 정상 표시
|
||||
- 운영서버(192.168.0.202)에서 외부 PC로 접속 시 이미지가 표시되지 않음
|
||||
- 브라우저 개발자 도구에서 이미지 요청 URL이 `http://localhost:8000/...`으로 되어 있음
|
||||
|
||||
### 원인 분석
|
||||
|
||||
#### 1차 원인: 서버2 소스 코드 미동기화
|
||||
|
||||
**문제**: 서버2의 프론트엔드 소스 코드가 개발서버와 동기화되지 않아 오래된 코드가 배포됨
|
||||
|
||||
| 위치 | 코드 |
|
||||
|------|------|
|
||||
| 서버4 (개발) | `process.env.NEXT_PUBLIC_API_URL \|\| 'http://localhost:8000'` |
|
||||
| 서버2 (운영) | `'http://localhost:8000'` (하드코딩) |
|
||||
|
||||
**영향받은 파일**:
|
||||
- `/admin/hero-banners/page.tsx`
|
||||
- `/admin/cars/page.tsx`
|
||||
- 기타 관리자 페이지
|
||||
|
||||
#### 2차 원인: 차량 상세 페이지 특수 패턴
|
||||
|
||||
**문제**: `/cars/[id]/page.tsx`에서 다른 방식으로 localhost가 하드코딩됨
|
||||
|
||||
```typescript
|
||||
// 문제 코드
|
||||
const getImageUrl = (url: string | undefined): string => {
|
||||
if (!url) return '';
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return url;
|
||||
}
|
||||
// 포트만 동적으로 추출하고 localhost는 하드코딩
|
||||
const backendPort = process.env.NEXT_PUBLIC_API_URL?.includes('8001') ? 8001 : 8000;
|
||||
return `http://localhost:${backendPort}${url}`;
|
||||
};
|
||||
```
|
||||
|
||||
### 해결 방법
|
||||
|
||||
#### Step 1: 소스 코드 동기화
|
||||
|
||||
```bash
|
||||
# 서버4 (Windows)에서 실행
|
||||
scp -r D:/Workspace/claudeCode/AutonetSellCar.com/frontend/src damon@192.168.0.202:/opt/autonet/production/frontend/
|
||||
```
|
||||
|
||||
#### Step 2: 차량 상세 페이지 수정
|
||||
|
||||
```typescript
|
||||
// 수정된 코드 (/cars/[id]/page.tsx)
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
|
||||
|
||||
const getImageUrl = (url: string | undefined): string => {
|
||||
if (!url) return '';
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return url;
|
||||
}
|
||||
return `${API_BASE_URL}${url}`;
|
||||
};
|
||||
```
|
||||
|
||||
#### Step 3: Docker 이미지 재빌드
|
||||
|
||||
```bash
|
||||
# 서버2에서 실행
|
||||
cd /opt/autonet/production/frontend
|
||||
|
||||
# 캐시 없이 재빌드 (중요!)
|
||||
docker build --no-cache -t autonet-frontend-v2 .
|
||||
|
||||
# 컨테이너 교체
|
||||
docker stop autonet-frontend
|
||||
docker rm autonet-frontend
|
||||
docker run -d --name autonet-frontend -p 3000:3000 autonet-frontend-v2
|
||||
```
|
||||
|
||||
#### Step 4: 확인
|
||||
|
||||
```bash
|
||||
# localhost:8000이 없는지 확인
|
||||
docker exec autonet-frontend sh -c "grep -l 'localhost:8000' /app/.next/static/chunks/app/**/*.js 2>/dev/null || echo 'No localhost:8000 found'"
|
||||
|
||||
# 올바른 URL이 있는지 확인
|
||||
docker exec autonet-frontend sh -c "grep -o '192.168.0.202:8000' /app/.next/static/chunks/app/**/*.js 2>/dev/null | wc -l"
|
||||
```
|
||||
|
||||
### 핵심 개념: Next.js 환경변수
|
||||
|
||||
> **중요**: Next.js의 `NEXT_PUBLIC_*` 환경변수는 **빌드 시점**에 JavaScript 번들에 포함됩니다.
|
||||
|
||||
| 시점 | 설명 |
|
||||
|------|------|
|
||||
| 빌드 시점 | `NEXT_PUBLIC_*` 변수 값이 JS 코드에 직접 삽입됨 |
|
||||
| 런타임 | 이미 삽입된 값이 사용됨 (변경 불가) |
|
||||
|
||||
따라서:
|
||||
- `.env.production` 수정 후 반드시 **재빌드** 필요
|
||||
- Docker 환경에서는 Dockerfile에 `ARG`/`ENV` 설정 필요
|
||||
|
||||
```dockerfile
|
||||
# Dockerfile 예시 (builder 단계)
|
||||
ARG NEXT_PUBLIC_API_URL=http://192.168.0.202:8000
|
||||
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||
RUN npm run build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 올바른 이미지 URL 패턴
|
||||
|
||||
모든 프론트엔드 파일에서 이미지 URL을 생성할 때 다음 패턴을 사용해야 합니다:
|
||||
|
||||
```typescript
|
||||
// 올바른 패턴
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
|
||||
|
||||
const getImageUrl = (url: string): string => {
|
||||
if (!url) return '';
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return url;
|
||||
}
|
||||
return `${API_BASE_URL}${url}`;
|
||||
};
|
||||
```
|
||||
|
||||
**잘못된 패턴 (사용 금지)**:
|
||||
```typescript
|
||||
// 하드코딩 - 절대 사용 금지!
|
||||
return `http://localhost:8000${url}`;
|
||||
|
||||
// 포트만 추출 - 사용 금지!
|
||||
const port = process.env.NEXT_PUBLIC_API_URL?.includes('8001') ? 8001 : 8000;
|
||||
return `http://localhost:${port}${url}`;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 디버깅 명령어 모음
|
||||
|
||||
### 프론트엔드 환경변수 확인
|
||||
|
||||
```bash
|
||||
# 컨테이너 환경변수
|
||||
docker exec autonet-frontend printenv | grep API
|
||||
|
||||
# .env.production 파일
|
||||
cat /opt/autonet/production/frontend/.env.production
|
||||
|
||||
# Dockerfile ARG/ENV 확인
|
||||
grep -E "(ARG|ENV).*NEXT_PUBLIC" /opt/autonet/production/frontend/Dockerfile
|
||||
```
|
||||
|
||||
### 빌드된 JS 파일 검사
|
||||
|
||||
```bash
|
||||
# localhost:8000 검색
|
||||
docker exec autonet-frontend sh -c "grep -r 'localhost:8000' /app/.next/static/chunks/ | head -5"
|
||||
|
||||
# 올바른 URL 검색
|
||||
docker exec autonet-frontend sh -c "grep -r '192.168.0.202:8000' /app/.next/static/chunks/ | head -5"
|
||||
```
|
||||
|
||||
### 이미지 직접 접근 테스트
|
||||
|
||||
```bash
|
||||
# 백엔드에서 이미지 직접 접근
|
||||
curl -I http://192.168.0.202:8000/uploads/cars/1/image_0.jpg
|
||||
|
||||
# API 응답에서 이미지 URL 확인
|
||||
curl -s http://192.168.0.202:8000/api/cars/1 | jq '.images'
|
||||
```
|
||||
|
||||
### 컨테이너 상태 확인
|
||||
|
||||
```bash
|
||||
docker ps -a | grep autonet
|
||||
docker logs autonet-frontend --tail 20
|
||||
docker logs autonet-backend --tail 20
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 예방 조치
|
||||
|
||||
### 1. 코드 동기화 자동화
|
||||
|
||||
개발 완료 후 운영 서버 배포 시 항상 최신 코드를 동기화:
|
||||
|
||||
```bash
|
||||
# 배포 전 동기화 스크립트
|
||||
scp -r frontend/src damon@192.168.0.202:/opt/autonet/production/frontend/
|
||||
```
|
||||
|
||||
### 2. 빌드 전 환경변수 확인
|
||||
|
||||
```bash
|
||||
# 빌드 전 확인 사항
|
||||
echo "=== .env.production ==="
|
||||
cat /opt/autonet/production/frontend/.env.production
|
||||
|
||||
echo "=== Dockerfile ENV ==="
|
||||
grep "NEXT_PUBLIC" /opt/autonet/production/frontend/Dockerfile
|
||||
```
|
||||
|
||||
### 3. 빌드 후 검증
|
||||
|
||||
```bash
|
||||
# 빌드 후 localhost 하드코딩 검사
|
||||
docker exec autonet-frontend sh -c "grep -r 'localhost:8000' /app/.next/static/chunks/ && echo 'ERROR: localhost found!' || echo 'OK: No localhost hardcoding'"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 체크리스트
|
||||
|
||||
배포 시 확인해야 할 항목:
|
||||
|
||||
- [ ] 서버2 소스 코드가 최신인가?
|
||||
- [ ] `.env.production`에 올바른 URL이 설정되어 있는가?
|
||||
- [ ] Dockerfile에 `ARG`/`ENV`가 설정되어 있는가?
|
||||
- [ ] `--no-cache` 옵션으로 빌드했는가?
|
||||
- [ ] 빌드된 JS에 `localhost:8000`이 없는가?
|
||||
- [ ] 외부 PC에서 이미지가 정상 표시되는가?
|
||||
|
||||
---
|
||||
|
||||
## 변경 이력
|
||||
|
||||
| 날짜 | 문제 | 해결 |
|
||||
|------|------|------|
|
||||
| 2024-12-30 | 운영서버 이미지 미표시 (배너) | 소스 코드 동기화 + Docker 재빌드 |
|
||||
| 2024-12-30 | 차량 상세 이미지 미표시 | `getImageUrl()` 함수 수정 |
|
||||
90
deploy_ru_server2.sh
Normal file
90
deploy_ru_server2.sh
Normal file
@@ -0,0 +1,90 @@
|
||||
#!/bin/bash
|
||||
# 서버2에서 러시아어 지원 배포 스크립트
|
||||
|
||||
echo "=== 1. DB 컬럼 추가 및 번역 업데이트 ==="
|
||||
|
||||
# Docker 컨테이너 내부에서 DB 업데이트
|
||||
docker exec -i autonet-backend python3 << 'EOF'
|
||||
import sqlite3
|
||||
conn = sqlite3.connect("/app/autonet.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 1. 컬럼 추가 (이미 있으면 무시)
|
||||
try:
|
||||
cursor.execute("ALTER TABLE hero_banners ADD COLUMN title_ru VARCHAR(100)")
|
||||
print("Added title_ru column")
|
||||
except:
|
||||
print("title_ru column already exists")
|
||||
|
||||
try:
|
||||
cursor.execute("ALTER TABLE hero_banners ADD COLUMN subtitle_ru VARCHAR(200)")
|
||||
print("Added subtitle_ru column")
|
||||
except:
|
||||
print("subtitle_ru column already exists")
|
||||
|
||||
conn.commit()
|
||||
|
||||
# 2. 러시아어 번역 추가
|
||||
translations = {
|
||||
"모하비 더 마스터": ("Mohave The Master", "Мохаве Мастер", "Мохаве Мастер"),
|
||||
"신형 K5(DL3)": ("New K5 (DL3)", "Шинэ K5 (DL3)", "Новый K5 (DL3)"),
|
||||
"더 뉴그랜드스타렉스": ("The New Grand Starex", "Шинэ Гранд Старекс", "Новый Гранд Старекс"),
|
||||
"더 뉴Santa Fe": ("The New Santa Fe", "Шинэ Санта Фе", "Новый Санта Фе"),
|
||||
"더 뉴 K7": ("The New K7", "Шинэ K7", "Новый K7"),
|
||||
}
|
||||
|
||||
cursor.execute("SELECT id, title_ko FROM hero_banners")
|
||||
for row in cursor.fetchall():
|
||||
banner_id, title_ko = row
|
||||
if title_ko in translations:
|
||||
en, mn, ru = translations[title_ko]
|
||||
cursor.execute(
|
||||
"UPDATE hero_banners SET title_en=?, title_mn=?, title_ru=? WHERE id=?",
|
||||
(en, mn, ru, banner_id)
|
||||
)
|
||||
print(f"Updated banner {banner_id}: {title_ko} -> RU: {ru}")
|
||||
|
||||
conn.commit()
|
||||
|
||||
# 3. 확인
|
||||
cursor.execute("SELECT id, title_ko, title_en, title_mn, title_ru FROM hero_banners")
|
||||
print("\n=== Current Banner Data ===")
|
||||
for row in cursor.fetchall():
|
||||
print(row)
|
||||
|
||||
print("\nDB Update Done!")
|
||||
EOF
|
||||
|
||||
echo ""
|
||||
echo "=== 2. Backend 컨테이너 재빌드 ==="
|
||||
cd /opt/autonet/production
|
||||
docker stop autonet-backend || true
|
||||
docker rm autonet-backend || true
|
||||
docker build -t autonet-backend-prod ./backend
|
||||
docker run -d --name autonet-backend \
|
||||
-p 8000:8000 \
|
||||
-v /opt/autonet/production/backend/uploads:/app/uploads \
|
||||
-v /opt/autonet/production/backend/autonet.db:/app/autonet.db \
|
||||
autonet-backend-prod
|
||||
|
||||
echo ""
|
||||
echo "=== 3. Frontend 컨테이너 재빌드 ==="
|
||||
docker stop autonet-frontend || true
|
||||
docker rm autonet-frontend || true
|
||||
docker build -t autonet-frontend-prod \
|
||||
--build-arg NEXT_PUBLIC_API_URL=http://192.168.0.202:8000 \
|
||||
./frontend
|
||||
docker run -d --name autonet-frontend \
|
||||
-p 3000:3000 \
|
||||
-e NEXT_PUBLIC_API_URL=http://192.168.0.202:8000 \
|
||||
autonet-frontend-prod
|
||||
|
||||
echo ""
|
||||
echo "=== 4. 결과 확인 ==="
|
||||
sleep 5
|
||||
echo "Testing Russian API..."
|
||||
curl -s 'http://localhost:8000/api/hero-banners/?lang=ru' | head -200
|
||||
|
||||
echo ""
|
||||
echo "=== 배포 완료 ==="
|
||||
docker ps
|
||||
48
update_db_ru.py
Normal file
48
update_db_ru.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import sqlite3
|
||||
conn = sqlite3.connect("/app/autonet.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 1. 컬럼 추가 (이미 있으면 무시)
|
||||
try:
|
||||
cursor.execute("ALTER TABLE hero_banners ADD COLUMN title_ru VARCHAR(100)")
|
||||
print("Added title_ru column")
|
||||
except:
|
||||
print("title_ru column already exists")
|
||||
|
||||
try:
|
||||
cursor.execute("ALTER TABLE hero_banners ADD COLUMN subtitle_ru VARCHAR(200)")
|
||||
print("Added subtitle_ru column")
|
||||
except:
|
||||
print("subtitle_ru column already exists")
|
||||
|
||||
conn.commit()
|
||||
|
||||
# 2. 러시아어 번역 추가
|
||||
translations = {
|
||||
"모하비 더 마스터": ("Mohave The Master", "Мохаве Мастер", "Мохаве Мастер"),
|
||||
"신형 K5(DL3)": ("New K5 (DL3)", "Шинэ K5 (DL3)", "Новый K5 (DL3)"),
|
||||
"더 뉴그랜드스타렉스": ("The New Grand Starex", "Шинэ Гранд Старекс", "Новый Гранд Старекс"),
|
||||
"더 뉴Santa Fe": ("The New Santa Fe", "Шинэ Санта Фе", "Новый Санта Фе"),
|
||||
"더 뉴 K7": ("The New K7", "Шинэ K7", "Новый K7"),
|
||||
}
|
||||
|
||||
cursor.execute("SELECT id, title_ko FROM hero_banners")
|
||||
for row in cursor.fetchall():
|
||||
banner_id, title_ko = row
|
||||
if title_ko in translations:
|
||||
en, mn, ru = translations[title_ko]
|
||||
cursor.execute(
|
||||
"UPDATE hero_banners SET title_en=?, title_mn=?, title_ru=? WHERE id=?",
|
||||
(en, mn, ru, banner_id)
|
||||
)
|
||||
print(f"Updated banner {banner_id}: {title_ko} -> EN: {en}, MN: {mn}, RU: {ru}")
|
||||
|
||||
conn.commit()
|
||||
|
||||
# 3. 확인
|
||||
cursor.execute("SELECT id, title_ko, title_en, title_mn, title_ru FROM hero_banners")
|
||||
print("\n=== Current Banner Data ===")
|
||||
for row in cursor.fetchall():
|
||||
print(row)
|
||||
|
||||
print("\nDone!")
|
||||
Reference in New Issue
Block a user