From e661d91c721c3c75d0518de4f35961f007ff81fb Mon Sep 17 00:00:00 2001 From: AutonetSellCar Deploy Date: Wed, 31 Dec 2025 10:41:42 +0900 Subject: [PATCH] fix: banner translations and deployment improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add translateCarName import from i18n.ts for proper multilingual support - Change default API language from 'ko' to 'en' for hero banners - Add checkbox column for Local Cars banner registration - Update Dockerfile with Playwright dependencies - Add PostgreSQL migration script - Add banner translation fix script πŸ€– Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: Claude --- DEPLOYMENT_GUIDE.md | 944 ++++++++++++++------------- backend/Dockerfile | 23 +- backend/app/api/hero_banners.py | 2 +- backend/app/config.py | 5 +- backend/app/main.py | 45 +- backend/fix_banner_translations.py | 124 ++++ backend/migrate_to_postgres.py | 297 +++++++++ backend/requirements.txt | 4 +- frontend/src/app/admin/cars/page.tsx | 189 ++++-- frontend/src/lib/api.ts | 2 +- 10 files changed, 1145 insertions(+), 490 deletions(-) create mode 100644 backend/fix_banner_translations.py create mode 100644 backend/migrate_to_postgres.py diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md index c416b12..f3f2c51 100644 --- a/DEPLOYMENT_GUIDE.md +++ b/DEPLOYMENT_GUIDE.md @@ -1,15 +1,326 @@ # AutonetSellCar.com 배포 κ°€μ΄λ“œ -이 λ¬Έμ„œλŠ” μ½”λ“œ μˆ˜μ •λΆ€ν„° 운영 λ°°ν¬κΉŒμ§€μ˜ 전체 과정을 μ„€λͺ…ν•©λ‹ˆλ‹€. +이 λ¬Έμ„œλŠ” μ‹œμŠ€ν…œ μ•„ν‚€ν…μ²˜μ™€ μ½”λ“œ μˆ˜μ •λΆ€ν„° 운영 λ°°ν¬κΉŒμ§€μ˜ 전체 과정을 μ„€λͺ…ν•©λ‹ˆλ‹€. --- -## 1. 전체 배포 흐름 κ°œμš” +## 1. μ‹œμŠ€ν…œ μ•„ν‚€ν…μ²˜ κ°œμš” ``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ 배포 νŒŒμ΄ν”„λΌμΈ 전체 흐름 β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ AutonetSellCar.com μ‹œμŠ€ν…œ μ•„ν‚€ν…μ²˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ 인터넷 β”‚ + β”‚ 59.14.158.123 β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ β”‚ + β–Ό β–Ό β–Ό + autonetsellcar.com autonetsellcar.com autonetsellcar.com + / /api /uploads + β”‚ β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ μ„œλ²„1 (192.168.0.201) β”‚ +β”‚ 인프라 μ„œλ²„ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Nginx Proxy β”‚ β”‚ PostgreSQL β”‚ β”‚ Redis β”‚ β”‚ Portainer/Grafana β”‚ β”‚ +β”‚ β”‚ Manager β”‚ β”‚ :5432 β”‚ β”‚ :6379 β”‚ β”‚ Prometheus β”‚ β”‚ +β”‚ β”‚ :80/:443 β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ DB: autonet β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ β”‚ + β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β–Ό β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ μ„œλ²„2 (192.168.0.202) β”‚ +β”‚ μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ„œλ²„ β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Production Environment β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ autonet-frontend β”‚ β”‚ autonet-backend β”‚ β”‚ carmodoo-agent β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ Next.js 14 │───▢│ FastAPI │───▢│ Playwright β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ :3000 β”‚ β”‚ :8000 β”‚ β”‚ PDF/Spec 쑰회 β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ PostgreSQL/Redis β”‚ β”‚ +β”‚ β”‚ └──────────────────────────────────────┼───┼──▢ μ„œλ²„1 +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Staging Environment β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ frontend-staging β”‚ β”‚ backend-staging β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ :3001 β”‚ β”‚ :8001 β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ μ„œλ²„4 (개발 PC) β”‚ +β”‚ β”‚ +β”‚ D:\Workspace\claudeCode\AutonetSellCar.com\ β”‚ +β”‚ β”œβ”€β”€ frontend/ (Next.js) β”‚ +β”‚ β”œβ”€β”€ backend/ (FastAPI) β”‚ +β”‚ └── carmodoo-agent/ β”‚ +β”‚ β”‚ +β”‚ 둜컬 ν…ŒμŠ€νŠΈ: localhost:3000 / localhost:8000 β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## 2. μ„œλ²„λ³„ μ—­ν•  및 ꡬ성 + +### μ„œλ²„1 (192.168.0.201) - 인프라 μ„œλ²„ + +| μ„œλΉ„μŠ€ | 포트 | μ—­ν•  | +|--------|------|------| +| Nginx Proxy Manager | 80, 443, 81 | λ¦¬λ²„μŠ€ ν”„λ‘μ‹œ, SSL μΈμ¦μ„œ 관리 | +| PostgreSQL | 5432 | 메인 λ°μ΄ν„°λ² μ΄μŠ€ (autonet) | +| Redis | 6379 | μΊμ‹œ, μ„Έμ…˜ 관리 | +| Portainer | 9000 | Docker 관리 UI | +| Prometheus | 9090 | λ©”νŠΈλ¦­ μˆ˜μ§‘ | +| Grafana | 3000 | λͺ¨λ‹ˆν„°λ§ λŒ€μ‹œλ³΄λ“œ | + +### μ„œλ²„2 (192.168.0.202) - μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ„œλ²„ + +| μ»¨ν…Œμ΄λ„ˆ | 포트 | μ—­ν•  | +|----------|------|------| +| autonet-frontend | 3000 | Next.js ν”„λ‘ νŠΈμ—”λ“œ (운영) | +| autonet-backend | 8000 | FastAPI λ°±μ—”λ“œ (운영) | +| carmodoo-agent | - | Playwright 기반 PDF/μŠ€νŽ™ 쑰회 | +| autonet-frontend-staging | 3001 | μŠ€ν…Œμ΄μ§• ν”„λ‘ νŠΈμ—”λ“œ | +| autonet-backend-staging | 8001 | μŠ€ν…Œμ΄μ§• λ°±μ—”λ“œ | + +--- + +## 3. 도메인 및 λΌμš°νŒ… μ„€μ • + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Nginx Proxy Manager μ„€μ • β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + autonetsellcar.com (SSL: Let's Encrypt) + β”‚ + β”œβ”€β”€ / ──▢ 192.168.0.202:3000 (Frontend) + β”‚ + β”œβ”€β”€ /api/* ──▢ 192.168.0.202:8000 (Backend) + β”‚ └── Custom Location: /api + β”‚ Forward: http://192.168.0.202:8000 + β”‚ + └── /uploads/* ──▢ 192.168.0.202:8000 (Backend Static) + └── Custom Location: /uploads + Forward: http://192.168.0.202:8000 + + μ„€μ • 방법: + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ 1. Nginx Proxy Manager (http://192.168.0.201:81) 접속 β”‚ + β”‚ 2. Proxy Hosts > autonetsellcar.com 선택 β”‚ + β”‚ 3. Custom Locations νƒ­μ—μ„œ /api, /uploads μΆ”κ°€ β”‚ + β”‚ - Location: /api β”‚ + β”‚ - Scheme: http β”‚ + β”‚ - Forward Hostname: 192.168.0.202 β”‚ + β”‚ - Forward Port: 8000 β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## 4. PostgreSQL μ„€μ • + +### 4.1 μ„œλ²„1 PostgreSQL Docker μ»¨ν…Œμ΄λ„ˆ + +```bash +# PostgreSQL μ»¨ν…Œμ΄λ„ˆ 정보 확인 +docker exec postgres-primary env | grep POSTGRES + +# 좜λ ₯: +# POSTGRES_USER=admin +# POSTGRES_PASSWORD=roskfl@1122 +# POSTGRES_DB=mongolcar +``` + +### 4.2 λ°μ΄ν„°λ² μ΄μŠ€ 생성 + +```bash +# autonet λ°μ΄ν„°λ² μ΄μŠ€ 생성 +docker exec -it postgres-primary psql -U admin -d postgres -c "CREATE DATABASE autonet;" + +# λΉ„λ°€λ²ˆν˜Έ μž¬μ„€μ • (ν•„μš”μ‹œ) +docker exec -it postgres-primary psql -U admin -d postgres -c "ALTER USER admin PASSWORD 'roskfl@1122';" +``` + +### 4.3 μ™ΈλΆ€ 접속 μ„€μ • 확인 + +```bash +# pg_hba.conf 확인 (μ™ΈλΆ€ 접속 ν—ˆμš©) +docker exec postgres-primary cat /var/lib/postgresql/data/pg_hba.conf | grep -v "^#" | grep -v "^$" + +# ν•„μˆ˜ μ„€μ •: +# host all all all scram-sha-256 +``` + +### 4.4 λ°±μ—”λ“œ PostgreSQL μ—°κ²° μ„€μ • + +**backend/.env:** +```env +# Database +USE_SQLITE=False +DB_HOST=192.168.0.201 +DB_PORT=5432 +DB_NAME=autonet +DB_USER=admin +DB_PASSWORD=roskfl@1122 +``` + +**backend/app/config.py:** +```python +@property +def DATABASE_URL(self) -> str: + if self.USE_SQLITE: + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + db_path = os.path.join(base_dir, "autonet.db") + return f"sqlite:///{db_path}" + # URL-encode password for special characters like @ # etc + from urllib.parse import quote_plus + encoded_password = quote_plus(self.DB_PASSWORD) + return f"postgresql://{self.DB_USER}:{encoded_password}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}" +``` + +### 4.5 SQLite β†’ PostgreSQL λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ + +```bash +# 개발 μ„œλ²„(μ„œλ²„4)μ—μ„œ μ‹€ν–‰ +cd D:\Workspace\claudeCode\AutonetSellCar.com\backend + +# psycopg2 μ„€μΉ˜ +venv\Scripts\pip.exe install psycopg2-binary + +# λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ 슀크립트 μ‹€ν–‰ +venv\Scripts\python.exe migrate_to_postgres.py +``` + +**λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ 슀크립트 μ£Όμš” κΈ°λŠ₯:** +- PostgreSQL에 ν…Œμ΄λΈ” μžλ™ 생성 (SQLAlchemy λͺ¨λΈ 기반) +- SQLite boolean (0/1) β†’ PostgreSQL boolean (true/false) λ³€ν™˜ +- FK μ œμ•½μ‘°κ±΄ μž„μ‹œ λΉ„ν™œμ„±ν™” ν›„ 데이터 이전 +- Sequence μžλ™ 리셋 + +--- + +## 5. Docker Compose μ„€μ • + +### 5.1 Docker Compose v2 μ„€μΉ˜ (μ„œλ²„2) + +```bash +# Docker 곡식 μ €μž₯μ†Œ μΆ”κ°€ +sudo apt-get update +sudo apt-get install ca-certificates curl gnupg -y +sudo install -m 0755 -d /etc/apt/keyrings +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg +sudo chmod a+r /etc/apt/keyrings/docker.gpg + +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + +# Docker Compose ν”ŒλŸ¬κ·ΈμΈ μ„€μΉ˜ +sudo apt-get update +sudo apt-get install docker-compose-plugin -y + +# 버전 확인 +docker compose version +``` + +### 5.2 docker-compose.production.yml + +```yaml +services: + frontend: + build: ./frontend + container_name: autonet-frontend + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - NEXT_PUBLIC_API_URL=https://autonetsellcar.com + depends_on: + - backend + networks: + - autonet-production-network + + backend: + build: ./backend + container_name: autonet-backend + ports: + - "8000:8000" + environment: + - ENV=production + env_file: + - ./backend/.env + volumes: + - ./backend/uploads:/app/uploads + networks: + - autonet-production-network + + carmodoo-agent: + build: ./carmodoo-agent + container_name: carmodoo-agent + networks: + - autonet-production-network + +networks: + autonet-production-network: + driver: bridge +``` + +### 5.3 μ£Όμš” Docker λͺ…λ Ήμ–΄ + +```bash +# 전체 μ‹œμž‘ +docker compose -f docker-compose.production.yml up -d + +# νŠΉμ • μ„œλΉ„μŠ€ μž¬λΉŒλ“œ (μΊμ‹œ 없이) +docker compose -f docker-compose.production.yml build backend --no-cache +docker compose -f docker-compose.production.yml up -d backend + +# 둜그 확인 +docker logs autonet-backend --tail 50 -f + +# μ»¨ν…Œμ΄λ„ˆ μƒνƒœ +docker ps + +# 전체 쀑지 및 제거 +docker compose -f docker-compose.production.yml down + +# λ„€νŠΈμ›Œν¬/λ³Όλ₯¨ 문제 μ‹œ +docker rm -f autonet-backend autonet-frontend carmodoo-agent +docker network rm autonet-production-network +docker compose -f docker-compose.production.yml up -d +``` + +--- + +## 6. 배포 흐름 + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ 배포 νŒŒμ΄ν”„λΌμΈ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ 1. 개발 β”‚ ───▢ β”‚ 2. 컀밋 β”‚ ───▢ β”‚ 3. μŠ€ν…Œμ΄μ§• β”‚ ───▢ β”‚ 4. 운영 β”‚ @@ -18,58 +329,89 @@ β”‚ β”‚ β”‚ β”‚ β–Ό β–Ό β–Ό β–Ό μ½”λ“œ μˆ˜μ • git commit Docker λΉŒλ“œ promote - 둜컬 ν…ŒμŠ€νŠΈ git push staging 포트 3001/8001 포트 3000/8000 + 둜컬 ν…ŒμŠ€νŠΈ git push staging :3001/:8001 :3000/:8000 +``` + +### Step 1: 개발 (μ„œλ²„4) + +```bash +# 둜컬 개발 μ„œλ²„ μ‹€ν–‰ +cd backend && venv\Scripts\activate +uvicorn app.main:app --reload --port 8000 + +cd frontend +npm run dev + +# ν…ŒμŠ€νŠΈ: http://localhost:3000 +``` + +### Step 2: Git Commit & Push + +```bash +git status +git add . +git commit -m "feat: κΈ°λŠ₯ μ„€λͺ…" +git push staging main +``` + +### Step 3: μŠ€ν…Œμ΄μ§• ν…ŒμŠ€νŠΈ + +```bash +# μ„œλ²„2 SSH 접속 ν›„ +cd /opt/autonet/staging +docker compose -f docker-compose.staging.yml build --no-cache +docker compose -f docker-compose.staging.yml up -d + +# ν…ŒμŠ€νŠΈ URL +# Frontend: http://192.168.0.202:3001 +# Backend: http://192.168.0.202:8001/docs +``` + +### Step 4: 운영 배포 + +```bash +# μ„œλ²„2μ—μ„œ +cd /opt/autonet/production +docker compose -f docker-compose.production.yml build --no-cache +docker compose -f docker-compose.production.yml up -d + +# λ˜λŠ” 슀크립트 μ‚¬μš© +/opt/autonet/scripts/deploy.sh promote + +# 운영 URL +# https://autonetsellcar.com +# https://autonetsellcar.com/api/docs ``` --- -## 2. μ„œλ²„ ν™˜κ²½ ꡬ성 - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ λ„€νŠΈμ›Œν¬ ꡬ성도 β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ μ„œλ²„4 β”‚ β”‚ μ„œλ²„2 β”‚ - β”‚ (개발 μ„œλ²„) β”‚ β”‚ (운영 μ„œλ²„) β”‚ - β”‚ β”‚ SSH/SCP β”‚ 192.168.0.202 β”‚ - β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ ─────────────────▢ β”‚ β”‚ - β”‚ β”‚ μ†ŒμŠ€ μ½”λ“œ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ - β”‚ β”‚ (둜컬 개발) β”‚ β”‚ β”‚ β”‚ Staging β”‚ β”‚ Production β”‚ β”‚ - β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ :3001/:8001β”‚ β”‚ :3000/:8000β”‚ β”‚ - β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ - β”‚ D:\Workspace\ β”‚ β”‚ β”‚ - β”‚ claudeCode\ β”‚ β”‚ /opt/autonet/ β”‚ - β”‚ AutonetSellCar.comβ”‚ β”‚ β”œβ”€β”€ staging/ β”‚ - β”‚ β”‚ β”‚ β”œβ”€β”€ production/ β”‚ - β”‚ β”‚ β”‚ └── git/autonet.git β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - ---- - -## 3. 디렉토리 ꡬ쑰 +## 7. 디렉토리 ꡬ쑰 ``` μ„œλ²„2 (/opt/autonet/) β”‚ β”œβ”€β”€ git/ -β”‚ └── autonet.git/ # Bare Git Repository +β”‚ └── autonet.git/ # Bare Git Repository β”‚ └── hooks/ -β”‚ └── post-receive # μžλ™ 배포 ν›… +β”‚ └── post-receive # μžλ™ 배포 ν›… β”‚ -β”œβ”€β”€ staging/ # μŠ€ν…Œμ΄μ§• ν™˜κ²½ +β”œβ”€β”€ staging/ # μŠ€ν…Œμ΄μ§• ν™˜κ²½ β”‚ β”œβ”€β”€ frontend/ β”‚ β”œβ”€β”€ backend/ +β”‚ β”‚ └── .env # μŠ€ν…Œμ΄μ§• ν™˜κ²½λ³€μˆ˜ β”‚ └── docker-compose.staging.yml β”‚ -β”œβ”€β”€ production/ # 운영 ν™˜κ²½ +β”œβ”€β”€ production/ # 운영 ν™˜κ²½ β”‚ β”œβ”€β”€ frontend/ β”‚ β”œβ”€β”€ backend/ +β”‚ β”‚ β”œβ”€β”€ .env # 운영 ν™˜κ²½λ³€μˆ˜ (PostgreSQL) +β”‚ β”‚ β”œβ”€β”€ requirements.txt # psycopg2-binary 포함 +β”‚ β”‚ └── app/ +β”‚ β”‚ └── config.py # URL 인코딩 적용 +β”‚ β”œβ”€β”€ carmodoo-agent/ β”‚ └── docker-compose.production.yml β”‚ -β”œβ”€β”€ releases/ # 둀백용 λ°±μ—… +β”œβ”€β”€ releases/ # 둀백용 λ°±μ—… β”‚ β”œβ”€β”€ 20241230_140000/ β”‚ └── 20241230_150000/ β”‚ @@ -80,445 +422,159 @@ --- -## 4. 단계별 상세 μ„€λͺ… +## 8. ν™˜κ²½λ³€μˆ˜ μ„€μ • -### Step 1: μ½”λ“œ μˆ˜μ • (μ„œλ²„4) +### 운영 μ„œλ²„ backend/.env 전체 -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Step 1: μ½”λ“œ μˆ˜μ • β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +```env +# Database (PostgreSQL) +USE_SQLITE=False +DB_HOST=192.168.0.201 +DB_PORT=5432 +DB_NAME=autonet +DB_USER=admin +DB_PASSWORD=roskfl@1122 - 개발자 PC (μ„œλ²„4) - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ β”‚ - β”‚ D:\Workspace\claudeCode\ β”‚ - β”‚ └── AutonetSellCar.com\ β”‚ - β”‚ β”œβ”€β”€ backend\ β”‚ - β”‚ β”‚ └── app\ β”‚ - β”‚ β”‚ β”œβ”€β”€ api\ ◀── μˆ˜μ • β”‚ - β”‚ β”‚ β”œβ”€β”€ models\ β”‚ - β”‚ β”‚ └── schemas\ ◀── μˆ˜μ • β”‚ - β”‚ β”‚ β”‚ - β”‚ └── frontend\ β”‚ - β”‚ └── src\ β”‚ - β”‚ β”œβ”€β”€ app\ ◀── μˆ˜μ • β”‚ - β”‚ β”œβ”€β”€ components\ ◀── μˆ˜μ • β”‚ - β”‚ └── lib\ ◀── μˆ˜μ • β”‚ - β”‚ β”‚ - β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ - β”‚ β”‚ 둜컬 ν…ŒμŠ€νŠΈ μ„œλ²„ β”‚ β”‚ - β”‚ β”‚ Frontend: http://localhost:3000β”‚ β”‚ - β”‚ β”‚ Backend: http://localhost:8000β”‚ β”‚ - β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ - β”‚ β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +# Redis +REDIS_HOST=192.168.0.201 +REDIS_PORT=6379 +REDIS_PASSWORD= - λͺ…λ Ήμ–΄: - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ # Backend μ‹€ν–‰ β”‚ - β”‚ cd backend β”‚ - β”‚ venv\Scripts\activate β”‚ - β”‚ uvicorn app.main:app --reload --port 8000β”‚ - β”‚ β”‚ - β”‚ # Frontend μ‹€ν–‰ β”‚ - β”‚ cd frontend β”‚ - β”‚ npm run dev β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` +# JWT +SECRET_KEY=your-secret-key-change-in-production -### Step 2: Git Commit & Push +# App +DEBUG=False -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Step 2: Git Commit & Push β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +# Azure Translator +AZURE_TRANSLATOR_KEY=your-azure-key +AZURE_TRANSLATOR_REGION=southeastasia - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ μ„œλ²„4 β”‚ β”‚ μ„œλ²„2 β”‚ - β”‚ (개발) β”‚ β”‚ (운영) β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ β”‚ - β”‚ 1. git add . β”‚ - β”‚ 2. git commit -m "message" β”‚ - β”‚ β”‚ - β–Ό β”‚ - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ - β”‚ Local Repo β”‚ β”‚ - β”‚ (main) β”‚ β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ - β”‚ β”‚ - β”‚ 3. git push staging main β”‚ - β”‚ β”‚ - β”‚ SSH (포트 22) β”‚ - β”‚ ════════════════════════════════════▢ β”‚ - β”‚ β–Ό - β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ β”‚ Bare Repo β”‚ - β”‚ β”‚ autonet.git β”‚ - β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ β”‚ - β”‚ β”‚ post-receive ν›… μ‹€ν–‰ - β”‚ β–Ό - β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ β”‚ μŠ€ν…Œμ΄μ§• β”‚ - β”‚ β”‚ μžλ™ 배포 β”‚ - β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +# Email (SMTP) +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=autonetsellcar@gmail.com +SMTP_PASSWORD=your-app-password +SMTP_FROM_EMAIL=autonetsellcar@gmail.com +SMTP_FROM_NAME=AutonetSellCar - λͺ…λ Ήμ–΄: - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ git status β”‚ - β”‚ git add . β”‚ - β”‚ git commit -m "feat: κΈ°λŠ₯ μ„€λͺ…" β”‚ - β”‚ git push staging main β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -### Step 3: μŠ€ν…Œμ΄μ§• ν…ŒμŠ€νŠΈ - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Step 3: μŠ€ν…Œμ΄μ§• ν…ŒμŠ€νŠΈ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - μ„œλ²„2 μŠ€ν…Œμ΄μ§• ν™˜κ²½ - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ β”‚ - β”‚ /opt/autonet/staging/ β”‚ - β”‚ β”‚ - β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ - β”‚ β”‚ Frontend Container β”‚ β”‚ Backend Container β”‚ β”‚ - β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ - β”‚ β”‚ autonet-frontend β”‚ β”‚ autonet-backend β”‚ β”‚ - β”‚ β”‚ -staging β”‚ β”‚ -staging β”‚ β”‚ - β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ - β”‚ β”‚ Port: 3001 ─────┼─────▢ Port: 8001 β”‚ β”‚ - β”‚ β”‚ β”‚ API β”‚ β”‚ β”‚ - β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ - β”‚ β”‚ β”‚ - β”‚ β–Ό β”‚ - β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ - β”‚ β”‚ staging-db β”‚ β”‚ - β”‚ β”‚ (SQLite/Volume) β”‚ β”‚ - β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ - β”‚ β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - ν…ŒμŠ€νŠΈ URL: - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Frontend: http://192.168.0.202:3001 β”‚ - β”‚ Backend: http://192.168.0.202:8001 β”‚ - β”‚ API Docs: http://192.168.0.202:8001/docsβ”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - μˆ˜λ™ λΉŒλ“œ λͺ…λ Ήμ–΄ (SSH 접속 ν›„): - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ cd /opt/autonet/staging β”‚ - β”‚ docker compose -f docker-compose.staging.yml downβ”‚ - β”‚ docker compose -f docker-compose.staging.yml build --no-cacheβ”‚ - β”‚ docker compose -f docker-compose.staging.yml up -dβ”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -### Step 4: 운영 배포 (Promote) - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Step 4: 운영 배포 (Promote) β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ μŠ€ν…Œμ΄μ§• β”‚ promote β”‚ 운영 β”‚ - β”‚ (ν…ŒμŠ€νŠΈ μ™„λ£Œ) β”‚ ══════════▢ β”‚ (μ„œλΉ„μŠ€ 쀑) β”‚ - β”‚ :3001 / :8001 β”‚ β”‚ :3000 / :8000 β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ β”‚ - β”‚ β–Ό - β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ β”‚ λ°±μ—… 생성 β”‚ - β”‚ β”‚ /releases/ β”‚ - β”‚ β”‚ 20241230_160000/ β”‚ - β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - 배포 흐름: - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ β”‚ - β”‚ 1. ν˜„μž¬ 운영 λ°±μ—… β”‚ - β”‚ └── /opt/autonet/releases/20241230_160000/ β”‚ - β”‚ β”‚ - β”‚ 2. 운영 μ»¨ν…Œμ΄λ„ˆ 쀑지 β”‚ - β”‚ └── docker compose down β”‚ - β”‚ β”‚ - β”‚ 3. μŠ€ν…Œμ΄μ§• β†’ 운영 볡사 (DB/uploads μ œμ™Έ) β”‚ - β”‚ └── rsync -av --exclude='*.db' staging/ production/ β”‚ - β”‚ β”‚ - β”‚ 4. 운영 μ»¨ν…Œμ΄λ„ˆ λΉŒλ“œ & μ‹œμž‘ β”‚ - β”‚ └── docker compose up -d --build β”‚ - β”‚ β”‚ - β”‚ 5. ν—¬μŠ€μ²΄ν¬ β”‚ - β”‚ └── curl http://localhost:3000 && curl http://localhost:8000 β”‚ - β”‚ β”‚ - β”‚ 6. μ‹€νŒ¨ μ‹œ μžλ™ λ‘€λ°± β”‚ - β”‚ └── deploy.sh rollback β”‚ - β”‚ β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - λͺ…λ Ήμ–΄: - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ # μŠ€ν…Œμ΄μ§• β†’ 운영 승격 β”‚ - β”‚ /opt/autonet/scripts/deploy.sh promote β”‚ - β”‚ β”‚ - β”‚ # λ‘€λ°± (문제 λ°œμƒ μ‹œ) β”‚ - β”‚ /opt/autonet/scripts/deploy.sh rollback β”‚ - β”‚ β”‚ - β”‚ # μƒνƒœ 확인 β”‚ - β”‚ /opt/autonet/scripts/deploy.sh status β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +# Verification +VERIFICATION_CODE_EXPIRE_MINUTES=10 +EMAIL_VERIFICATION_REQUIRED=True ``` --- -## 5. μˆ˜λ™ 배포 방법 (SCP 직접 전솑) +## 9. 문제 ν•΄κ²° -κΈ΄κΈ‰ 배포 λ˜λŠ” post-receive ν›… λ―Έμ„€μ • μ‹œ μ‚¬μš©ν•©λ‹ˆλ‹€. +### PostgreSQL μ—°κ²° 였λ₯˜ + +```bash +# μ—λŸ¬: could not translate host name "1122@192.168.0.201" +# 원인: λΉ„λ°€λ²ˆν˜Έμ— @ λ¬Έμžκ°€ URL κ΅¬λΆ„μžλ‘œ 인식됨 +# ν•΄κ²°: config.pyμ—μ„œ URL 인코딩 적용 + +from urllib.parse import quote_plus +encoded_password = quote_plus(self.DB_PASSWORD) +``` + +### Docker λ„€νŠΈμ›Œν¬ 좩돌 + +```bash +# μ—λŸ¬: network was found but has incorrect label +docker compose -f docker-compose.production.yml down +docker network rm autonet-production-network +docker compose -f docker-compose.production.yml up -d +``` + +### μ»¨ν…Œμ΄λ„ˆ 이름 좩돌 + +```bash +# μ—λŸ¬: container name is already in use +docker rm -f autonet-backend autonet-frontend carmodoo-agent +docker compose -f docker-compose.production.yml up -d +``` + +### psycopg2 λͺ¨λ“ˆ μ—†μŒ + +```bash +# requirements.txt에 μΆ”κ°€ +psycopg2-binary # PostgreSQL production + +# λ˜λŠ” 직접 μΆ”κ°€ +echo "psycopg2-binary" >> /opt/autonet/production/backend/requirements.txt +docker compose -f docker-compose.production.yml build backend --no-cache +``` + +### Mixed Content μ—λŸ¬ ``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ μˆ˜λ™ 배포 (SCP 방식) β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - μ„œλ²„4 (개발) μ„œλ²„2 (운영) - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ β”‚ β”‚ β”‚ - β”‚ μˆ˜μ •λœ 파일 β”‚ SCP 전솑 β”‚ production/ β”‚ - β”‚ frontend/ β”‚ ═══════════════════════▢│ frontend/ β”‚ - β”‚ backend/ β”‚ β”‚ backend/ β”‚ - β”‚ β”‚ β”‚ β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Docker μž¬λΉŒλ“œ β”‚ - β”‚ β”‚ - β”‚ docker build β”‚ - β”‚ docker run β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - Step 1: 파일 전솑 (PowerShellμ—μ„œ) - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ # Frontend 파일 전솑 β”‚ - β”‚ scp -r frontend/src/app/admin/*.tsx \ β”‚ - β”‚ damon@192.168.0.202:/opt/autonet/production/frontend/src/app/admin/β”‚ - β”‚ β”‚ - β”‚ # Backend 파일 전솑 β”‚ - β”‚ scp -r backend/app/* \ β”‚ - β”‚ damon@192.168.0.202:/opt/autonet/production/backend/app/ β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - Step 2: Docker μž¬λΉŒλ“œ (μ„œλ²„2 SSH 접속 ν›„) - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ cd /opt/autonet/production β”‚ - β”‚ β”‚ - β”‚ # Frontend μž¬λΉŒλ“œ β”‚ - β”‚ docker stop autonet-frontend β”‚ - β”‚ docker rm autonet-frontend β”‚ - β”‚ 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 β”‚ - β”‚ β”‚ - β”‚ # Backend μž¬λΉŒλ“œ β”‚ - β”‚ docker stop autonet-backend β”‚ - β”‚ docker rm autonet-backend β”‚ - β”‚ docker build -t autonet-backend-prod ./backend β”‚ - β”‚ docker run -d --name autonet-backend \ β”‚ - β”‚ -p 8000:8000 \ β”‚ - β”‚ -v $(pwd)/backend/uploads:/app/uploads \ β”‚ - β”‚ -v $(pwd)/backend/autonet.db:/app/autonet.db \ β”‚ - β”‚ autonet-backend-prod β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +# μ—λŸ¬: Mixed Content - HTTPS νŽ˜μ΄μ§€μ—μ„œ HTTP API 호좜 +# ν•΄κ²°: Frontend λΉŒλ“œ μ‹œ HTTPS API URL μ‚¬μš© +NEXT_PUBLIC_API_URL=https://autonetsellcar.com ``` --- -## 6. Docker μ»¨ν…Œμ΄λ„ˆ ꡬ성 +## 10. λΉ λ₯Έ μ°Έμ‘° λͺ…λ Ήμ–΄ ``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Docker μ»¨ν…Œμ΄λ„ˆ ꡬ성도 β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +╔═══════════════════════════════════════════════════════════════════════════════╗ +β•‘ 개발 (μ„œλ²„4) β•‘ +╠═══════════════════════════════════════════════════════════════════════════════╣ +β•‘ git status # λ³€κ²½ 파일 확인 β•‘ +β•‘ git add . # μŠ€ν…Œμ΄μ§• β•‘ +β•‘ git commit -m "message" # 컀밋 β•‘ +β•‘ git push staging main # μ„œλ²„2둜 ν‘Έμ‹œ β•‘ +β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• - 운영 ν™˜κ²½ (Production) - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ β”‚ - β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ - β”‚ β”‚ Docker Network β”‚ β”‚ - β”‚ β”‚ β”‚ β”‚ - β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ - β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ - β”‚ β”‚ β”‚ autonet-frontendβ”‚ API β”‚ autonet-backend β”‚ β”‚ β”‚ - β”‚ β”‚ β”‚ │───────▢│ β”‚ β”‚ β”‚ - β”‚ β”‚ β”‚ Next.js 14 β”‚ β”‚ FastAPI β”‚ β”‚ β”‚ - β”‚ β”‚ β”‚ Port: 3000 β”‚ β”‚ Port: 8000 β”‚ β”‚ β”‚ - β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ - β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ - β”‚ β”‚ β”‚ β”‚ β”‚ - β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ - β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ - β”‚ β”‚ β”‚ Volumes β”‚ β”‚ β”‚ - β”‚ β”‚ β”‚ - autonet.db β”‚ β”‚ β”‚ - β”‚ β”‚ β”‚ - uploads/ β”‚ β”‚ β”‚ - β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ - β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ - β”‚ β”‚ β”‚ β”‚ - β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ - β”‚ β”‚ - β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ - β”‚ β”‚ carmodoo-agent β”‚ (Playwright λΈŒλΌμš°μ € μžλ™ν™”) β”‚ - β”‚ β”‚ - PDF 생성 β”‚ β”‚ - β”‚ β”‚ - μ°¨λŸ‰ μŠ€νŽ™ 쑰회 β”‚ β”‚ - β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ - β”‚ β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +╔═══════════════════════════════════════════════════════════════════════════════╗ +β•‘ 운영 (μ„œλ²„2) β•‘ +╠═══════════════════════════════════════════════════════════════════════════════╣ +β•‘ docker ps # μ‹€ν–‰ 쀑인 μ»¨ν…Œμ΄λ„ˆ β•‘ +β•‘ docker logs autonet-backend --tail 50 # λ°±μ—”λ“œ 둜그 β•‘ +β•‘ docker compose -f docker-compose.production.yml up -d # μ‹œμž‘ β•‘ +β•‘ docker compose -f docker-compose.production.yml down # 쀑지 β•‘ +β•‘ docker compose -f docker-compose.production.yml build --no-cache # μž¬λΉŒλ“œ β•‘ +β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• - 포트 λ§€ν•‘: - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ 호슀트:3000 ──▢ μ»¨ν…Œμ΄λ„ˆ:3000 (Frontend)β”‚ - β”‚ 호슀트:8000 ──▢ μ»¨ν…Œμ΄λ„ˆ:8000 (Backend) β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +╔═══════════════════════════════════════════════════════════════════════════════╗ +β•‘ PostgreSQL (μ„œλ²„1) β•‘ +╠═══════════════════════════════════════════════════════════════════════════════╣ +β•‘ docker exec -it postgres-primary psql -U admin -d autonet # DB 접속 β•‘ +β•‘ \dt # ν…Œμ΄λΈ” λͺ©λ‘ β•‘ +β•‘ \q # μ’…λ£Œ β•‘ +β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• ``` --- -## 7. λ‘€λ°± 절차 +## 11. 체크리슀트 -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ λ‘€λ°± 절차 β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +### 졜초 μ„œλ²„ μ„€μ • - 문제 λ°œμƒ! - β”‚ - β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” μ•„λ‹ˆμ˜€ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ μžλ™ λ‘€λ°± β”‚ ◀────────────── β”‚ ν—¬μŠ€μ²΄ν¬ 톡과?β”‚ - β”‚ 싀행됨 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ 예 - β”‚ β–Ό - β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ β”‚ 배포 μ™„λ£Œ β”‚ - β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ β”‚ - β”‚ /opt/autonet/releases/ β”‚ - β”‚ β”‚ β”‚ - β”‚ β”œβ”€β”€ 20241230_140000/ ◀── 이전 λ²„μ „μœΌλ‘œ 볡원 β”‚ - β”‚ β”œβ”€β”€ 20241230_150000/ β”‚ - β”‚ └── 20241230_160000/ ◀── ν˜„μž¬ (문제 λ°œμƒ) β”‚ - β”‚ β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +- [ ] μ„œλ²„1: PostgreSQL μ»¨ν…Œμ΄λ„ˆ μ‹€ν–‰ 쀑 +- [ ] μ„œλ²„1: autonet λ°μ΄ν„°λ² μ΄μŠ€ 생성 +- [ ] μ„œλ²„1: μ™ΈλΆ€ 접속 ν—ˆμš© (pg_hba.conf) +- [ ] μ„œλ²„1: Nginx Proxy Manager SSL μ„€μ • +- [ ] μ„œλ²„1: Custom Location (/api, /uploads) μ„€μ • +- [ ] μ„œλ²„2: Docker Compose v2 μ„€μΉ˜ +- [ ] μ„œλ²„2: backend/.env PostgreSQL μ„€μ • +- [ ] μ„œλ²„2: requirements.txt에 psycopg2-binary μΆ”κ°€ +- [ ] μ„œλ²„2: config.py URL 인코딩 적용 - λ‘€λ°± λͺ…λ Ήμ–΄: - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ # 직전 λ²„μ „μœΌλ‘œ λ‘€λ°± β”‚ - β”‚ /opt/autonet/scripts/deploy.sh rollback β”‚ - β”‚ β”‚ - β”‚ # νŠΉμ • λ²„μ „μœΌλ‘œ λ‘€λ°± β”‚ - β”‚ /opt/autonet/scripts/deploy.sh \ β”‚ - β”‚ rollback-to 20241230_140000 β”‚ - β”‚ β”‚ - β”‚ # μ‚¬μš© κ°€λŠ₯ν•œ 버전 확인 β”‚ - β”‚ ls /opt/autonet/releases/ β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` +### 배포 μ „ ---- +- [ ] 둜컬 ν…ŒμŠ€νŠΈ μ™„λ£Œ +- [ ] git status둜 λ³€κ²½ 파일 확인 +- [ ] 컀밋 λ©”μ‹œμ§€ μž‘μ„± -## 8. λΉ λ₯Έ μ°Έμ‘° λͺ…λ Ήμ–΄ +### 배포 ν›„ -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ λΉ λ₯Έ μ°Έμ‘° λͺ…λ Ήμ–΄ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - ╔═══════════════════════════════════════════════════════════════════════════╗ - β•‘ 개발 (μ„œλ²„4) β•‘ - ╠═══════════════════════════════════════════════════════════════════════════╣ - β•‘ git status # λ³€κ²½ 파일 확인 β•‘ - β•‘ git add . # μŠ€ν…Œμ΄μ§• β•‘ - β•‘ git commit -m "message" # 컀밋 β•‘ - β•‘ git push staging main # μ„œλ²„2둜 ν‘Έμ‹œ β•‘ - β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• - - ╔═══════════════════════════════════════════════════════════════════════════╗ - β•‘ 운영 (μ„œλ²„2) β•‘ - ╠═══════════════════════════════════════════════════════════════════════════╣ - β•‘ docker ps # μ‹€ν–‰ 쀑인 μ»¨ν…Œμ΄λ„ˆ 확인 β•‘ - β•‘ docker logs autonet-frontend # ν”„λ‘ νŠΈμ—”λ“œ 둜그 β•‘ - β•‘ docker logs autonet-backend # λ°±μ—”λ“œ 둜그 β•‘ - β•‘ docker restart autonet-frontend # ν”„λ‘ νŠΈμ—”λ“œ μž¬μ‹œμž‘ β•‘ - β•‘ docker restart autonet-backend # λ°±μ—”λ“œ μž¬μ‹œμž‘ β•‘ - β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• - - ╔═══════════════════════════════════════════════════════════════════════════╗ - β•‘ 배포 슀크립트 β•‘ - ╠═══════════════════════════════════════════════════════════════════════════╣ - β•‘ ./deploy.sh promote # μŠ€ν…Œμ΄μ§• β†’ 운영 승격 β•‘ - β•‘ ./deploy.sh rollback # 직전 버전 λ‘€λ°± β•‘ - β•‘ ./deploy.sh rollback-to # νŠΉμ • 버전 λ‘€λ°± β•‘ - β•‘ ./deploy.sh status # ν˜„μž¬ μƒνƒœ 확인 β•‘ - β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• -``` - ---- - -## 9. 체크리슀트 - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ 배포 μ „ 체크리슀트 β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - 배포 μ „: - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ [ ] λ‘œμ»¬μ—μ„œ ν…ŒμŠ€νŠΈ μ™„λ£Œ β”‚ - β”‚ [ ] git status둜 λ³€κ²½ 파일 확인 β”‚ - β”‚ [ ] λΆˆν•„μš”ν•œ 파일 μ œμ™Έ (.env, node_modules λ“±) β”‚ - β”‚ [ ] 컀밋 λ©”μ‹œμ§€ μž‘μ„± β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - μŠ€ν…Œμ΄μ§• ν…ŒμŠ€νŠΈ: - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ [ ] Frontend 정상 μž‘λ™ (http://192.168.0.202:3001) β”‚ - β”‚ [ ] Backend API 정상 μž‘λ™ (http://192.168.0.202:8001/docs) β”‚ - β”‚ [ ] μ£Όμš” κΈ°λŠ₯ ν…ŒμŠ€νŠΈ μ™„λ£Œ β”‚ - β”‚ [ ] μ—λŸ¬ 둜그 확인 (docker logs) β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - 운영 배포 ν›„: - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ [ ] ν—¬μŠ€μ²΄ν¬ 톡과 β”‚ - β”‚ [ ] μ£Όμš” νŽ˜μ΄μ§€ 접속 확인 β”‚ - β”‚ [ ] API 응닡 확인 β”‚ - β”‚ [ ] 이전 버전 λ°±μ—… 확인 (/opt/autonet/releases/) β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - ---- - -## 10. 문제 ν•΄κ²° - -| 문제 | ν•΄κ²° 방법 | -|------|----------| -| μ»¨ν…Œμ΄λ„ˆ μ‹œμž‘ μ•ˆλ¨ | `docker logs ` 둜그 확인 | -| 포트 좩돌 | `netstat -tlnp \| grep ` 확인 ν›„ ν”„λ‘œμ„ΈμŠ€ μ’…λ£Œ | -| 이미지 λΉŒλ“œ μ‹€νŒ¨ | `docker build --no-cache` μΊμ‹œ 없이 μž¬λΉŒλ“œ | -| DB μ—°κ²° 였λ₯˜ | Volume 마운트 확인, 파일 κΆŒν•œ 확인 | -| API 404 였λ₯˜ | Backend μ»¨ν…Œμ΄λ„ˆ μž¬μ‹œμž‘, λΌμš°ν„° 등둝 확인 | +- [ ] docker ps둜 μ»¨ν…Œμ΄λ„ˆ μƒνƒœ 확인 +- [ ] docker logs둜 μ—λŸ¬ 확인 +- [ ] https://autonetsellcar.com 접속 ν…ŒμŠ€νŠΈ +- [ ] API 응닡 확인 (/api/docs) --- diff --git a/backend/Dockerfile b/backend/Dockerfile index eb3faa5..5c6bd6a 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,16 +2,37 @@ FROM python:3.11-slim WORKDIR /app -# Install system dependencies +# Install system dependencies for PostgreSQL and Playwright RUN apt-get update && apt-get install -y \ gcc \ libpq-dev \ + # Playwright dependencies + libnss3 \ + libnspr4 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libcups2 \ + libdrm2 \ + libdbus-1-3 \ + libxkbcommon0 \ + libatspi2.0-0 \ + libxcomposite1 \ + libxdamage1 \ + libxfixes3 \ + libxrandr2 \ + libgbm1 \ + libasound2 \ + libpango-1.0-0 \ + libcairo2 \ && rm -rf /var/lib/apt/lists/* # Install Python dependencies COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt +# Install Playwright browsers (Chromium only for smaller image) +RUN playwright install chromium + # Copy application code COPY . . diff --git a/backend/app/api/hero_banners.py b/backend/app/api/hero_banners.py index e1294a5..604b5e7 100644 --- a/backend/app/api/hero_banners.py +++ b/backend/app/api/hero_banners.py @@ -40,7 +40,7 @@ def get_localized_field(obj, field: str, lang: str) -> Optional[str]: @router.get("/", response_model=List[HeroBannerLocalizedResponse]) def get_hero_banners( - lang: str = Query("ko", regex="^(ko|en|mn|ru)$"), + lang: str = Query("en", regex="^(ko|en|mn|ru)$"), db: Session = Depends(get_db) ): """ν™œμ„± νžˆμ–΄λ‘œ λ°°λ„ˆ λͺ©λ‘ 쑰회 (Public)""" diff --git a/backend/app/config.py b/backend/app/config.py index aa837be..cff6ee6 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -64,7 +64,10 @@ class Settings(BaseSettings): base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) db_path = os.path.join(base_dir, "autonet.db") return f"sqlite:///{db_path}" - return f"postgresql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}" + # URL-encode password for special characters like @ # etc + from urllib.parse import quote_plus + encoded_password = quote_plus(self.DB_PASSWORD) + return f"postgresql://{self.DB_USER}:{encoded_password}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}" @property def REDIS_URL(self) -> str: diff --git a/backend/app/main.py b/backend/app/main.py index 5f20e79..efdfe4b 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,6 +1,8 @@ -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.responses import RedirectResponse from contextlib import asynccontextmanager import os import asyncio @@ -15,6 +17,42 @@ from datetime import datetime, timedelta app_settings = get_settings() + +class TrailingSlashMiddleware(BaseHTTPMiddleware): + """ + Middleware to normalize trailing slashes on API paths. + Uses redirect to strip trailing slashes from non-root routes. + + Routes defined with "/" (like /hero-banners/) keep trailing slash. + Routes defined without "/" (like /cars) get redirected. + """ + # Routes that are defined WITH trailing slash (router.get("/")) + TRAILING_SLASH_ROUTES = { + "/api/hero-banners/", + "/api/settings/", + "/api/notifications/", + "/api/vehicle-requests/", + } + + async def dispatch(self, request: Request, call_next): + path = request.url.path + + # Skip if it's a known trailing-slash route + if path in self.TRAILING_SLASH_ROUTES: + return await call_next(request) + + # Redirect trailing slash from other /api/* paths + if path.startswith("/api/") and path.endswith("/") and len(path) > 5: + new_path = path.rstrip("/") + if request.url.query: + new_url = f"{new_path}?{request.url.query}" + else: + new_url = new_path + return RedirectResponse(url=new_url, status_code=307) + + return await call_next(request) + + # Create tables Base.metadata.create_all(bind=engine) @@ -118,6 +156,9 @@ app = FastAPI( lifespan=lifespan ) +# Trailing slash middleware (must be added before CORS) +app.add_middleware(TrailingSlashMiddleware) + # CORS - credentials=True requires explicit origins (not "*") app.add_middleware( CORSMiddleware, @@ -126,6 +167,8 @@ app.add_middleware( "http://127.0.0.1:3000", "http://localhost:8000", "http://192.168.0.202:3000", # Local network + "https://autonetsellcar.com", # Production + "http://autonetsellcar.com", # Production (HTTP redirect) ], allow_credentials=True, allow_methods=["*"], diff --git a/backend/fix_banner_translations.py b/backend/fix_banner_translations.py new file mode 100644 index 0000000..3838d1a --- /dev/null +++ b/backend/fix_banner_translations.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +""" +Fix hero banner translations by updating title_en, title_mn, title_ru from title_ko +""" +import sqlite3 +import os + +# Translation dictionary (subset of key terms) +TRANSLATIONS = { + # Makers + 'ν˜„λŒ€': {'en': 'Hyundai', 'mn': 'Π₯Ρ‘Π½Π΄Π°ΠΉ', 'ru': 'Π₯Ρ‘Π½Π΄Π°ΠΉ'}, + 'κΈ°μ•„': {'en': 'Kia', 'mn': 'Киа', 'ru': 'Киа'}, + 'μ œλ„€μ‹œμŠ€': {'en': 'Genesis', 'mn': 'ЖСнСзис', 'ru': 'ДТСнСзис'}, + 'μ‰λ³΄λ ˆ': {'en': 'Chevrolet', 'mn': 'Π¨Π΅Π²Ρ€ΠΎΠ»Π΅', 'ru': 'Π¨Π΅Π²Ρ€ΠΎΠ»Π΅'}, + 'KGλͺ¨λΉŒλ¦¬ν‹°': {'en': 'KG Mobility', 'mn': 'ΠšΠ– ΠœΠΎΠ±ΠΈΠ»ΠΈΡ‚ΠΈ', 'ru': 'ΠšΠ“ ΠœΠΎΠ±ΠΈΠ»ΠΈΡ‚ΠΈ'}, + '쌍용': {'en': 'SsangYong', 'mn': 'БсангЁнг', 'ru': 'БсангЙонг'}, + + # Models + 'λͺ¨ν•˜λΉ„': {'en': 'Mohave', 'mn': 'ΠœΠΎΡ…Π°Π²Π΅', 'ru': 'ΠœΠΎΡ…Π°Π²Π΅'}, + '더 λ§ˆμŠ€ν„°': {'en': 'The Master', 'mn': 'ΠœΠ°ΡΡ‚Π΅Ρ€', 'ru': 'ΠœΠ°ΡΡ‚Π΅Ρ€'}, + 'μ‹ ν˜•': {'en': 'New', 'mn': 'Шинэ', 'ru': 'Новый'}, + '더 뉴': {'en': 'The New', 'mn': 'Шинэ', 'ru': 'Новый'}, + 'κ·Έλžœλ“œμŠ€νƒ€λ ‰μŠ€': {'en': 'Grand Starex', 'mn': 'Π“Ρ€Π°Π½Π΄ БтарСкс', 'ru': 'Π“Ρ€Π°Π½Π΄ БтарСкс'}, + 'μŠ€νƒ€λ ‰μŠ€': {'en': 'Starex', 'mn': 'БтарСкс', 'ru': 'БтарСкс'}, + 'μ‹Όνƒ€νŽ˜': {'en': 'Santa Fe', 'mn': 'Π‘Π°Π½Ρ‚Π° Π€Π΅', 'ru': 'Π‘Π°Π½Ρ‚Π° Π€Π΅'}, + 'μŠ€νŒ…μ–΄': {'en': 'Stinger', 'mn': 'Π‘Ρ‚ΠΈΠ½Π³Π΅Ρ€', 'ru': 'Π‘Ρ‚ΠΈΠ½Π³Π΅Ρ€'}, + 'λ§ˆμ΄μŠ€ν„°': {'en': 'Meister', 'mn': 'ΠœΠ΅ΠΉΡΡ‚Π΅Ρ€', 'ru': 'ΠœΠ΅ΠΉΡΡ‚Π΅Ρ€'}, + 'μ˜λ Œν† ': {'en': 'Sorento', 'mn': 'Π‘ΠΎΡ€Π΅Π½Ρ‚ΠΎ', 'ru': 'Π‘ΠΎΡ€Π΅Π½Ρ‚ΠΎ'}, + 'μŠ€ν¬ν‹°μ§€': {'en': 'Sportage', 'mn': 'Π‘ΠΏΠΎΡ€Ρ‚Π°ΠΆ', 'ru': 'Π‘ΠΏΠΎΡ€Ρ‚Π°ΠΆ'}, + 'μΉ΄λ‹ˆλ°œ': {'en': 'Carnival', 'mn': 'ΠšΠ°Ρ€Π½ΠΈΠ²Π°Π»', 'ru': 'ΠšΠ°Ρ€Π½ΠΈΠ²Π°Π»'}, + 'μ…€ν† μŠ€': {'en': 'Seltos', 'mn': 'БСлтос', 'ru': 'БСлтос'}, + 'νˆ¬μ‹Ό': {'en': 'Tucson', 'mn': 'Вуксон', 'ru': 'Вуксон'}, + 'νŒ°λ¦¬μ„Έμ΄λ“œ': {'en': 'Palisade', 'mn': 'ПалисСйд', 'ru': 'ПалисСйд'}, + 'μ•„λ°˜λ–Ό': {'en': 'Avante', 'mn': 'АвантС', 'ru': 'АвантС'}, + 'μ˜λ‚˜νƒ€': {'en': 'Sonata', 'mn': 'Π‘ΠΎΠ½Π°Ρ‚Π°', 'ru': 'Π‘ΠΎΠ½Π°Ρ‚Π°'}, + 'κ·Έλžœμ €': {'en': 'Grandeur', 'mn': 'Π“Ρ€Π°Π½Π΄Ρ‘Ρ€', 'ru': 'Π“Ρ€Π°Π½Π΄Ρ‘Ρ€'}, + 'μ½”λ‚˜': {'en': 'Kona', 'mn': 'Кона', 'ru': 'Кона'}, + 'K5': {'en': 'K5', 'mn': 'K5', 'ru': 'K5'}, + 'K3': {'en': 'K3', 'mn': 'K3', 'ru': 'K3'}, + 'K7': {'en': 'K7', 'mn': 'K7', 'ru': 'K7'}, + 'K8': {'en': 'K8', 'mn': 'K8', 'ru': 'K8'}, + 'K9': {'en': 'K9', 'mn': 'K9', 'ru': 'K9'}, + 'GV70': {'en': 'GV70', 'mn': 'GV70', 'ru': 'GV70'}, + 'GV80': {'en': 'GV80', 'mn': 'GV80', 'ru': 'GV80'}, + 'G70': {'en': 'G70', 'mn': 'G70', 'ru': 'G70'}, + 'G80': {'en': 'G80', 'mn': 'G80', 'ru': 'G80'}, + 'G90': {'en': 'G90', 'mn': 'G90', 'ru': 'G90'}, +} + +# Sort keys by length (longest first) to avoid partial matches +SORTED_KEYS = sorted(TRANSLATIONS.keys(), key=len, reverse=True) + + +def translate(text: str, lang: str) -> str: + """Translate Korean text to target language""" + if not text: + return text + + result = text + for key in SORTED_KEYS: + if key in result: + translation = TRANSLATIONS[key].get(lang, key) + result = result.replace(key, translation) + return result + + +def main(): + # Find database file + db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "autonet.db") + + if not os.path.exists(db_path): + print(f"Database not found at: {db_path}") + return + + print(f"Using database: {db_path}") + + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # Get all banners + cursor.execute("SELECT id, title_ko, title_en, title_mn, title_ru FROM hero_banners") + banners = cursor.fetchall() + + print(f"\nFound {len(banners)} banners to update:\n") + + for banner in banners: + banner_id, title_ko, title_en, title_mn, title_ru = banner + + if not title_ko: + print(f"Banner {banner_id}: No Korean title, skipping") + continue + + new_title_en = translate(title_ko, 'en') + new_title_mn = translate(title_ko, 'mn') + new_title_ru = translate(title_ko, 'ru') + + print(f"Banner {banner_id}:") + print(f" KO: {title_ko}") + print(f" EN: {title_en} -> {new_title_en}") + print(f" MN: {title_mn} -> {new_title_mn}") + print(f" RU: {title_ru} -> {new_title_ru}") + print() + + # Update the banner + cursor.execute(""" + UPDATE hero_banners + SET title_en = ?, title_mn = ?, title_ru = ? + WHERE id = ? + """, (new_title_en, new_title_mn, new_title_ru, banner_id)) + + conn.commit() + print(f"Updated {len(banners)} banners successfully!") + + # Verify + print("\n--- Verification ---") + cursor.execute("SELECT id, title_ko, title_en, title_mn FROM hero_banners") + for row in cursor.fetchall(): + print(f"ID {row[0]}: {row[1]} -> EN: {row[2]}, MN: {row[3]}") + + conn.close() + + +if __name__ == "__main__": + main() diff --git a/backend/migrate_to_postgres.py b/backend/migrate_to_postgres.py new file mode 100644 index 0000000..3961c41 --- /dev/null +++ b/backend/migrate_to_postgres.py @@ -0,0 +1,297 @@ +""" +SQLite to PostgreSQL Migration Script +Handles boolean conversion and foreign key constraints +""" +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import sqlite3 +import psycopg2 +from urllib.parse import quote_plus + +SQLITE_PATH = os.path.join(os.path.dirname(__file__), "autonet.db") +PG_CONFIG = { + "host": "192.168.0.201", + "port": 5432, + "database": "autonet", + "user": "admin", + "password": "roskfl@1122" +} + +# Tables in dependency order (parents first) +TABLES_ORDER = [ + # Base tables (no FK dependencies) + "car_makers", + "car_models", + "translations", + "system_settings", + "cc_packages", + "exchange_rates", + "exchange_rate_history", + "hero_banner_settings", + # Users before user-dependent tables + "users", + # Car related + "cars", + "car_images", + "car_options", + "car_performance_checks", + "car_specifications", + "car_views", + "performance_check_views", + # Cache + "car_cache", + "car_detail_cache", + "cache_request_queue", + # Hero banners + "hero_banners", + # User activities + "charge_history", + "inquiries", + "inquiry_messages", + "vehicle_requests", + "request_vehicles", + "purchased_vehicles", + "dealer_applications", + "dealer_info", + "vehicle_shares", + "share_rewards", + "withdrawal_requests", + "referral_rewards", + "notifications", + "push_subscriptions", + "user_notification_preferences", + "verification_codes", + "visitor_logs", + "visitor_daily_stats", + "visitor_sessions", +] + +def create_tables(): + """Create tables in PostgreSQL""" + print("\n[Step 1] Creating tables in PostgreSQL...") + + os.environ["USE_SQLITE"] = "False" + os.environ["DB_HOST"] = PG_CONFIG["host"] + os.environ["DB_PORT"] = str(PG_CONFIG["port"]) + os.environ["DB_NAME"] = PG_CONFIG["database"] + os.environ["DB_USER"] = PG_CONFIG["user"] + os.environ["DB_PASSWORD"] = PG_CONFIG["password"] + + from sqlalchemy import create_engine + from app.database import Base + from app.models import ( + CarMaker, CarModel, Car, CarImage, CarOption, + User, CarView, PerformanceCheckView, ChargeHistory, VerificationCode, + Inquiry, InquiryMessage, HeroBanner, HeroBannerSettings, + Translation, CarCache, CarDetailCache, CacheRequestQueue, + SystemSettings, VehicleRequest, RequestVehicle, PurchasedVehicle, + DealerApplication, DealerInfo, VehicleShare, ShareReward, + WithdrawalRequest, ReferralReward, Notification, + PushSubscription, UserNotificationPreference, + CarPerformanceCheck, CarSpecification, + ExchangeRate, ExchangeRateHistory, CCPackage, + VisitorLog, VisitorDailyStats, VisitorSession, + ) + + encoded_pw = quote_plus(PG_CONFIG['password']) + pg_url = f"postgresql://{PG_CONFIG['user']}:{encoded_pw}@{PG_CONFIG['host']}:{PG_CONFIG['port']}/{PG_CONFIG['database']}" + engine = create_engine(pg_url, echo=False) + Base.metadata.create_all(bind=engine) + print(" Tables created successfully!") + +def get_boolean_columns(pg_cursor, table_name): + """Get list of boolean columns for a table""" + pg_cursor.execute(""" + SELECT column_name FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = %s AND data_type = 'boolean' + """, (table_name,)) + return [row[0] for row in pg_cursor.fetchall()] + +def convert_row(row, columns, bool_cols): + """Convert SQLite row values for PostgreSQL (handle booleans)""" + result = [] + for i, val in enumerate(row): + col_name = columns[i] + if col_name in bool_cols and val is not None: + # Convert 0/1 to False/True + result.append(bool(val)) + else: + result.append(val) + return tuple(result) + +def migrate_table(sqlite_conn, pg_conn, table_name): + """Migrate a single table""" + sqlite_cursor = sqlite_conn.cursor() + pg_cursor = pg_conn.cursor() + + # Get SQLite columns + sqlite_cursor.execute(f"PRAGMA table_info({table_name})") + sqlite_cols = [col[1] for col in sqlite_cursor.fetchall()] + if not sqlite_cols: + return 0 + + # Get data + sqlite_cursor.execute(f"SELECT * FROM {table_name}") + rows = sqlite_cursor.fetchall() + if not rows: + print(f" {table_name}: 0 rows (empty)") + return 0 + + # Check PostgreSQL table exists + pg_cursor.execute(""" + SELECT EXISTS (SELECT FROM information_schema.tables + WHERE table_schema = 'public' AND table_name = %s) + """, (table_name,)) + if not pg_cursor.fetchone()[0]: + print(f" {table_name}: skipped (not in PostgreSQL)") + return 0 + + # Get PostgreSQL columns + pg_cursor.execute(""" + SELECT column_name FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = %s + ORDER BY ordinal_position + """, (table_name,)) + pg_cols = [row[0] for row in pg_cursor.fetchall()] + + # Find common columns + common_cols = [c for c in sqlite_cols if c in pg_cols] + if not common_cols: + print(f" {table_name}: skipped (no matching columns)") + return 0 + + col_indices = [sqlite_cols.index(c) for c in common_cols] + + # Get boolean columns + bool_cols = set(get_boolean_columns(pg_cursor, table_name)) + + # Prepare query + cols_str = ", ".join(common_cols) + placeholders = ", ".join(["%s"] * len(common_cols)) + insert_sql = f"INSERT INTO {table_name} ({cols_str}) VALUES ({placeholders})" + + try: + # Truncate with CASCADE to handle FK + pg_cursor.execute(f"TRUNCATE TABLE {table_name} CASCADE") + + success = 0 + for row in rows: + try: + # Extract and convert row + filtered = tuple(row[i] for i in col_indices) + converted = convert_row(filtered, common_cols, bool_cols) + pg_cursor.execute(insert_sql, converted) + success += 1 + except Exception as e: + pg_conn.rollback() + # Truncate again after rollback + pg_cursor.execute(f"TRUNCATE TABLE {table_name} CASCADE") + print(f" {table_name}: error - {str(e)[:80]}") + return 0 + + pg_conn.commit() + print(f" {table_name}: {success}/{len(rows)} rows migrated") + return success + except Exception as e: + pg_conn.rollback() + print(f" {table_name}: error - {str(e)[:80]}") + return 0 + +def reset_sequences(pg_conn): + """Reset sequences to max(id) + 1""" + print("\n[Step 3] Resetting sequences...") + pg_cursor = pg_conn.cursor() + + # Get all tables with id column + pg_cursor.execute(""" + SELECT table_name FROM information_schema.columns + WHERE table_schema = 'public' AND column_name = 'id' + """) + tables = [row[0] for row in pg_cursor.fetchall()] + + for table in tables: + try: + # Check if sequence exists + seq_name = f"{table}_id_seq" + pg_cursor.execute(""" + SELECT EXISTS (SELECT FROM pg_sequences WHERE schemaname = 'public' AND sequencename = %s) + """, (seq_name,)) + if pg_cursor.fetchone()[0]: + pg_cursor.execute(f""" + SELECT setval('{seq_name}', COALESCE((SELECT MAX(id) FROM {table}), 0) + 1, false) + """) + except: + pass + + pg_conn.commit() + print(" Sequences reset completed") + +def main(): + print("=" * 60) + print("SQLite to PostgreSQL Migration") + print("=" * 60) + + # Step 1: Create tables + try: + create_tables() + except Exception as e: + print(f" Failed: {e}") + sys.exit(1) + + # Step 2: Connect and migrate + print("\n[Step 2] Migrating data...") + + sqlite_conn = sqlite3.connect(SQLITE_PATH) + + encoded_pw = quote_plus(PG_CONFIG['password']) + pg_conn = psycopg2.connect( + host=PG_CONFIG['host'], + port=PG_CONFIG['port'], + database=PG_CONFIG['database'], + user=PG_CONFIG['user'], + password=PG_CONFIG['password'] + ) + + # Disable FK checks during migration + pg_cursor = pg_conn.cursor() + pg_cursor.execute("SET session_replication_role = 'replica';") + pg_conn.commit() + + # Get SQLite tables + sqlite_cursor = sqlite_conn.cursor() + sqlite_cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name != 'sqlite_sequence'") + all_tables = set(t[0] for t in sqlite_cursor.fetchall()) + + # Migrate in order + total = 0 + migrated_tables = set() + + for table in TABLES_ORDER: + if table in all_tables: + total += migrate_table(sqlite_conn, pg_conn, table) + migrated_tables.add(table) + + # Migrate remaining tables + remaining = all_tables - migrated_tables + for table in remaining: + total += migrate_table(sqlite_conn, pg_conn, table) + + # Re-enable FK checks + pg_cursor.execute("SET session_replication_role = 'origin';") + pg_conn.commit() + + # Step 3: Reset sequences + reset_sequences(pg_conn) + + sqlite_conn.close() + pg_conn.close() + + print("\n" + "=" * 60) + print(f"Migration completed! Total rows: {total}") + print("=" * 60) + +if __name__ == "__main__": + main() diff --git a/backend/requirements.txt b/backend/requirements.txt index 3abb92c..a8bd45b 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,7 +1,7 @@ fastapi uvicorn[standard] sqlalchemy -# psycopg2-binary # Uncomment for PostgreSQL production +psycopg2-binary # PostgreSQL production redis python-dotenv pydantic @@ -15,6 +15,8 @@ lxml alembic email-validator playwright # PDF capture for performance check reports +img2pdf # Convert screenshots to PDF +pillow # Image processing for PDF generation apscheduler # Scheduled tasks (exchange rate updates) stripe # Payment processing user-agents # Visitor tracking diff --git a/frontend/src/app/admin/cars/page.tsx b/frontend/src/app/admin/cars/page.tsx index c4d5bdf..58c1b47 100644 --- a/frontend/src/app/admin/cars/page.tsx +++ b/frontend/src/app/admin/cars/page.tsx @@ -4,6 +4,7 @@ import { useState, useEffect } from 'react'; import { useSearchParams, useRouter } from 'next/navigation'; import Image from 'next/image'; import api, { heroBannersApi, carmodooApi, vehicleRequestsApi } from '@/lib/api'; +import { translateCarName } from '@/lib/i18n'; interface CarmodooMaker { code: string; @@ -130,6 +131,8 @@ export default function CarsAdminPage() { const [selectedCar, setSelectedCar] = useState(null); const [showDetailModal, setShowDetailModal] = useState(false); const [currentImageIndex, setCurrentImageIndex] = useState(0); + const [selectedLocalCars, setSelectedLocalCars] = useState>(new Set()); + const [registeringLocalBanner, setRegisteringLocalBanner] = useState(false); // All Cars (public view) state const [allCars, setAllCars] = useState([]); @@ -549,30 +552,6 @@ export default function CarsAdminPage() { } }; - // μ°¨λŸ‰λͺ… λ²ˆμ—­ ν•¨μˆ˜ - const translateCarName = (koreanName: string | undefined): string => { - if (!koreanName) return '-'; - - const translations: Record = { - 'ν˜„λŒ€': 'Hyundai', 'μ œλ„€μ‹œμŠ€': 'Genesis', 'κΈ°μ•„': 'Kia', - 'μ‰λ³΄λ ˆ(λŒ€μš°)': 'Chevrolet', 'μ‰λ³΄λ ˆ': 'Chevrolet', - 'λ₯΄λ…Έ(μ‚Όμ„±)': 'Renault', 'KGλͺ¨λΉŒλ¦¬ν‹°(쌍용)': 'KG Mobility', - 'λ‹›μ‚°': 'Nissan', 'λ ‰μ„œμŠ€': 'Lexus', 'ν† μš”νƒ€': 'Toyota', 'ν˜Όλ‹€': 'Honda', - 'μ˜λ Œν† ': 'Sorento', 'μŠ€ν¬ν‹°μ§€': 'Sportage', 'μ…€ν† μŠ€': 'Seltos', - 'μΉ΄λ‹ˆλ°œ': 'Carnival', 'λͺ¨λ‹': 'Morning', '레이': 'Ray', - 'μ•„λ°˜λ–Ό': 'Avante', 'μ˜λ‚˜νƒ€': 'Sonata', 'κ·Έλžœμ €': 'Grandeur', - 'νˆ¬μ‹Ό': 'Tucson', 'μ‹Όνƒ€νŽ˜': 'Santa Fe', 'νŒ°λ¦¬μ„Έμ΄λ“œ': 'Palisade', - 'μ½”λ‚˜': 'Kona', 'μŠ€νƒ€λ¦¬μ•„': 'Staria', '캐슀퍼': 'Casper', - }; - - let result = koreanName; - const sortedKeys = Object.keys(translations).sort((a, b) => b.length - a.length); - for (const korean of sortedKeys) { - result = result.replace(new RegExp(korean, 'g'), translations[korean]); - } - return result; - }; - // λ”œλŸ¬ μ„€λͺ… 미리보기 및 νŽΈμ§‘ ν•¨μˆ˜ const handleEditDealerDescription = async (car: CarmodooCarItem) => { setEditingCar(car); @@ -700,11 +679,13 @@ export default function CarsAdminPage() { const bannerData = { title_ko: car.car_name || '', - title_en: translateCarName(car.car_name), - title_mn: translateCarName(car.car_name), + title_en: translateCarName(car.car_name, 'en'), + title_mn: translateCarName(car.car_name, 'mn'), + title_ru: translateCarName(car.car_name, 'ru'), subtitle_ko: `${car.year}년식 | ${car.mileage?.toLocaleString()}km`, subtitle_en: `${car.year} | ${car.mileage?.toLocaleString()}km`, subtitle_mn: `${car.year} | ${car.mileage?.toLocaleString()}km`, + subtitle_ru: `${car.year} | ${car.mileage?.toLocaleString()}km`, image_url: localImageUrl, link_url: `/cars/${carId}`, is_active: true, @@ -752,6 +733,80 @@ export default function CarsAdminPage() { } }; + // Local Carsμ—μ„œ λ°°λ„ˆ λ“±λ‘ν•˜λŠ” ν•¨μˆ˜ + const handleRegisterLocalCarAsBanner = async () => { + if (selectedLocalCars.size === 0) { + alert('Please select at least one car to register as banner.'); + return; + } + + if (!confirm(`${selectedLocalCars.size}개의 μ°¨λŸ‰μ„ Hero Banner둜 λ“±λ‘ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?`)) { + return; + } + + setRegisteringLocalBanner(true); + try { + const selectedCarsList = localCars.filter(car => selectedLocalCars.has(car.id)); + const existingBanners = await heroBannersApi.adminGetList(); + let orderStart = existingBanners.length; + let successCount = 0; + + for (const car of selectedCarsList) { + const localImageUrl = `/uploads/cars/${car.id}/image_0.jpg`; + + const bannerData = { + title_ko: car.car_name || '', + title_en: translateCarName(car.car_name || '', 'en'), + title_mn: translateCarName(car.car_name || '', 'mn'), + title_ru: translateCarName(car.car_name || '', 'ru'), + subtitle_ko: `${car.year || ''}년식 | ${car.mileage ? formatMileage(car.mileage) : ''}`, + subtitle_en: `${car.year || ''} | ${car.mileage ? formatMileage(car.mileage) : ''}`, + subtitle_mn: `${car.year || ''} | ${car.mileage ? formatMileage(car.mileage) : ''}`, + subtitle_ru: `${car.year || ''} | ${car.mileage ? formatMileage(car.mileage) : ''}`, + image_url: localImageUrl, + link_url: `/cars/${car.id}`, + display_order: orderStart++, + is_active: true, + car_id: car.id, + }; + + await heroBannersApi.adminCreate(bannerData); + successCount++; + } + + alert(`${successCount}개의 μ°¨λŸ‰μ΄ Hero Banner둜 λ“±λ‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€.`); + setSelectedLocalCars(new Set()); + } catch (err) { + console.error('Local banner registration failed:', err); + alert('λ°°λ„ˆ 등둝에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.'); + } finally { + setRegisteringLocalBanner(false); + } + }; + + // Local car selection toggle + const handleLocalCarSelect = (carId: number, e: React.MouseEvent) => { + e.stopPropagation(); + setSelectedLocalCars(prev => { + const newSet = new Set(prev); + if (newSet.has(carId)) { + newSet.delete(carId); + } else { + newSet.add(carId); + } + return newSet; + }); + }; + + // Select all local cars + const handleSelectAllLocalCars = () => { + if (selectedLocalCars.size === localCars.length) { + setSelectedLocalCars(new Set()); + } else { + setSelectedLocalCars(new Set(localCars.map(car => car.id))); + } + }; + // μ°¨λŸ‰ μΆ”μ²œ λͺ©λ‘μ— μΆ”κ°€ ν•¨μˆ˜ (Vehicle Request용) const handleAddToRequest = async () => { if (!requestId) return; @@ -1029,17 +1084,45 @@ export default function CarsAdminPage() {

Imported Cars ({localTotal} total) + {selectedLocalCars.size > 0 && ( + + ({selectedLocalCars.size} selected) + + )}

- +
+ {selectedLocalCars.size > 0 && ( + + )} + +
{localLoading ? ( @@ -1066,6 +1149,14 @@ export default function CarsAdminPage() { + @@ -1088,6 +1179,24 @@ export default function CarsAdminPage() { className={`border-b border-gray-100 hover:bg-gray-50 cursor-pointer ${!car.is_displayed ? 'opacity-60' : ''}`} onClick={() => handleCarClick(car)} > +
+ 0} + onChange={handleSelectAllLocalCars} + className="w-4 h-4 text-purple-600 rounded border-gray-300 focus:ring-purple-500" + /> + Display Image Car Name e.stopPropagation()}> + { + setSelectedLocalCars(prev => { + const newSet = new Set(prev); + if (newSet.has(car.id)) { + newSet.delete(car.id); + } else { + newSet.add(car.id); + } + return newSet; + }); + }} + className="w-4 h-4 text-purple-600 rounded border-gray-300 focus:ring-purple-500" + /> +