diff --git a/DEPLOYMENT_CHECKLIST.md b/DEPLOYMENT_CHECKLIST.md new file mode 100644 index 0000000..8be6900 --- /dev/null +++ b/DEPLOYMENT_CHECKLIST.md @@ -0,0 +1,230 @@ +# AutonetSellCar 배포 체크리스트 + +## 환경 정보 + +| 항목 | 서버4 (개발) | 서버2 (운영) | +|------|-------------|-------------| +| IP | 192.168.0.204 | 192.168.0.202 | +| OS | Windows | Ubuntu 22.04 | +| Frontend 포트 | 3000 | 3000 (운영), 3001 (스테이징) | +| Backend 포트 | 8000 | 8000 (운영), 8001 (스테이징) | + +--- + +## 1. 서버2 디렉토리 구조 + +``` +/opt/autonet/ +├── git/autonet.git/ # Git bare repository +├── staging/ # 스테이징 코드 +├── production/ # 운영 코드 +│ ├── frontend/ +│ │ ├── Dockerfile +│ │ ├── .env.production # ★ NEXT_PUBLIC_API_URL 설정 +│ │ └── ... +│ ├── backend/ +│ │ ├── Dockerfile +│ │ ├── autonet.db # ★ 데이터베이스 +│ │ └── uploads/ # ★ 이미지 파일 +│ └── docker-compose.production.yml +├── releases/ # 롤백용 아카이브 +├── scripts/ # 배포 스크립트 +└── logs/ # 로그 +``` + +--- + +## 2. 핵심 설정 파일 + +### 2.1 Frontend 환경변수 (.env.production) + +**파일 위치**: `/opt/autonet/production/frontend/.env.production` + +```env +NEXT_PUBLIC_API_URL=http://192.168.0.202:8000 +``` + +⚠️ **중요**: Next.js는 **빌드 시점**에 `NEXT_PUBLIC_*` 변수를 코드에 포함시킵니다. +- 런타임에 환경변수를 바꿔도 적용 안됨 +- 환경변수 변경 후 반드시 **이미지 재빌드** 필요 + +### 2.2 Dockerfile (Frontend) + +**파일 위치**: `/opt/autonet/production/frontend/Dockerfile` + +빌드 시 환경변수 적용을 위해 다음이 포함되어야 함: + +```dockerfile +# builder 단계에서 +ARG NEXT_PUBLIC_API_URL=http://192.168.0.202:8000 +ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL +``` + +### 2.3 docker-compose.production.yml + +```yaml +version: '3.8' +services: + frontend: + build: ./frontend + container_name: autonet-frontend + ports: + - "3000:3000" + environment: + - NODE_ENV=production + depends_on: + - backend + + backend: + build: ./backend + container_name: autonet-backend + ports: + - "8000:8000" + volumes: + - ./backend/uploads:/app/uploads # ★ 이미지 영속화 + - ./backend/autonet.db:/app/autonet.db # ★ DB 영속화 +``` + +--- + +## 3. 데이터 이전 체크리스트 + +### 3.1 데이터베이스 (autonet.db) + +| 단계 | 명령어 | 확인 | +|------|--------|------| +| 서버4에서 복사 | `scp backend/autonet.db damon@192.168.0.202:/tmp/` | [ ] | +| 서버2 컨테이너에 복사 | `docker cp /tmp/autonet.db autonet-backend:/app/` | [ ] | +| 파일 확인 | `docker exec autonet-backend ls -la /app/autonet.db` | [ ] | +| 백엔드 재시작 | `docker restart autonet-backend` | [ ] | + +### 3.2 업로드 이미지 (uploads/) + +| 단계 | 명령어 | 확인 | +|------|--------|------| +| 서버4에서 압축 | `tar -cvf uploads.tar uploads` | [ ] | +| 서버2로 복사 | `scp uploads.tar damon@192.168.0.202:/tmp/` | [ ] | +| 압축 해제 | `cd /tmp && tar -xvf uploads.tar` | [ ] | +| 컨테이너에 복사 | `docker cp /tmp/uploads/. autonet-backend:/app/uploads/` | [ ] | +| 파일 확인 | `docker exec autonet-backend ls -la /app/uploads/` | [ ] | + +--- + +## 4. 이미지 URL 문제 디버깅 + +### 4.1 현재 문제 + +브라우저에서 이미지 요청 URL이 `http://localhost:8000/...`으로 되어 있음. +→ 다른 PC에서 접속하면 localhost가 그 PC를 가리키므로 이미지 안 보임. + +### 4.2 올바른 이미지 URL + +``` +http://192.168.0.202:8000/uploads/cars/39/image_0.jpg +``` + +### 4.3 원인 분석 + +1. **api.ts에서 API_BASE_URL 설정 확인** + - 파일: `frontend/src/lib/api.ts` + - `NEXT_PUBLIC_API_URL` 환경변수 사용 여부 + +2. **이미지 URL 생성 로직 확인** + - 이미지 URL이 어디서 생성되는지 + - 백엔드에서 전체 URL 반환? vs 프론트엔드에서 조합? + +3. **빌드 시점 환경변수 확인** + - `.env.production` 파일 존재 여부 + - Dockerfile에서 ARG/ENV 설정 여부 + +--- + +## 5. 디버깅 명령어 + +### 5.1 프론트엔드 환경변수 확인 + +```bash +# 컨테이너 환경변수 +docker exec autonet-frontend printenv | grep API + +# .env.production 파일 내용 +cat /opt/autonet/production/frontend/.env.production +``` + +### 5.2 백엔드 API 테스트 + +```bash +# 이미지 직접 접근 테스트 +curl -I http://192.168.0.202:8000/uploads/cars/39/image_0.jpg + +# API 응답에서 이미지 URL 확인 +curl http://192.168.0.202:8000/api/cars/39 | jq '.images' +``` + +### 5.3 컨테이너 상태 확인 + +```bash +docker ps -a +docker logs autonet-frontend --tail 20 +docker logs autonet-backend --tail 20 +``` + +--- + +## 6. 재빌드 절차 + +### 6.1 Frontend 재빌드 (환경변수 변경 시) + +```bash +cd /opt/autonet/production + +# 1. 컨테이너 중지/삭제 +docker rm -f autonet-frontend + +# 2. 이미지 삭제 +docker rmi production_frontend + +# 3. .env.production 확인 +cat frontend/.env.production + +# 4. 재빌드 +docker build --no-cache -t production_frontend ./frontend + +# 5. 실행 +docker run -d \ + --name autonet-frontend \ + --network autonet-production-network \ + -p 3000:3000 \ + production_frontend + +# 6. 확인 +docker ps | grep frontend +docker logs autonet-frontend --tail 10 +``` + +--- + +## 7. 현재 상태 점검 명령어 + +서버2에서 순서대로 실행: + +```bash +# 1. 컨테이너 상태 +docker ps -a + +# 2. 프론트엔드 환경변수 +docker exec autonet-frontend printenv | grep -E "(API|NODE)" + +# 3. .env.production 파일 +cat /opt/autonet/production/frontend/.env.production + +# 4. Dockerfile 환경변수 설정 +grep -A2 "ARG NEXT_PUBLIC" /opt/autonet/production/frontend/Dockerfile +grep "ENV NEXT_PUBLIC" /opt/autonet/production/frontend/Dockerfile + +# 5. 백엔드 이미지 파일 존재 확인 +docker exec autonet-backend ls /app/uploads/cars/ | head -5 + +# 6. 백엔드 이미지 직접 접근 테스트 +curl -I http://localhost:8000/uploads/cars/39/image_0.jpg +``` diff --git a/backend/uploads.tar b/backend/uploads.tar new file mode 100644 index 0000000..5a7bd70 Binary files /dev/null and b/backend/uploads.tar differ diff --git a/frontend/src/app/admin/cars/page.tsx b/frontend/src/app/admin/cars/page.tsx index ad5c069..ed63b78 100644 --- a/frontend/src/app/admin/cars/page.tsx +++ b/frontend/src/app/admin/cars/page.tsx @@ -103,13 +103,15 @@ const FUEL_TYPES = [ { value: 'LPG', label: 'LPG' }, ]; +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'; + // 이미지 URL 변환 (로컬 경로는 백엔드 URL 추가) const getImageUrl = (url: string | undefined): string => { if (!url) return ''; if (url.startsWith('http://') || url.startsWith('https://')) { return url; } - return `http://localhost:8000${url}`; + return `${API_BASE_URL}${url}`; }; const YEAR_OPTIONS = Array.from({ length: 15 }, (_, i) => 2024 - i); diff --git a/frontend/src/app/admin/hero-banners/page.tsx b/frontend/src/app/admin/hero-banners/page.tsx index 77b313c..391df25 100644 --- a/frontend/src/app/admin/hero-banners/page.tsx +++ b/frontend/src/app/admin/hero-banners/page.tsx @@ -33,13 +33,15 @@ const defaultFormData: BannerFormData = { car_id: null, }; +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'; + // 이미지 URL 변환 (로컬 경로는 백엔드 URL 추가) const getImageUrl = (url: string | undefined): string => { if (!url) return ''; if (url.startsWith('http://') || url.startsWith('https://')) { return url; } - return `http://localhost:8000${url}`; + return `${API_BASE_URL}${url}`; }; export default function HeroBannersPage() { @@ -97,7 +99,7 @@ export default function HeroBannersPage() { .map((b: any) => b.car_id); if (carIds.length > 0) { try { - const response = await fetch('http://localhost:8000/api/carmodoo/pdf-status', { + const response = await fetch(`${API_BASE_URL}/api/carmodoo/pdf-status`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(carIds), diff --git a/frontend/src/components/FilmStripSlider.tsx b/frontend/src/components/FilmStripSlider.tsx index 83a6c03..e28af9e 100644 --- a/frontend/src/components/FilmStripSlider.tsx +++ b/frontend/src/components/FilmStripSlider.tsx @@ -324,6 +324,8 @@ interface BannerCardProps { height: number; } +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'; + // 이미지 URL 변환 (로컬 경로는 백엔드 URL 추가) const getImageUrl = (url: string): string => { if (!url) return ''; @@ -331,7 +333,7 @@ const getImageUrl = (url: string): string => { return url; } // 로컬 경로인 경우 백엔드 URL 추가 - return `http://localhost:8000${url}`; + return `${API_BASE_URL}${url}`; }; // Helper to get localized title/subtitle based on language