commit bf87175f515287fd25d175843915ffa6178025eb Author: Wind Date: Tue Feb 10 12:23:22 2026 +0900 AssetPilot OrangePi 5 Pluse Server-First Commit diff --git a/.TemporaryDocument/DOCKER_QUICKSTART.md b/.TemporaryDocument/DOCKER_QUICKSTART.md new file mode 100644 index 0000000..0004e58 --- /dev/null +++ b/.TemporaryDocument/DOCKER_QUICKSTART.md @@ -0,0 +1,227 @@ +# ๐Ÿณ Asset Pilot Docker ๋น ๋ฅธ ์‹œ์ž‘ + +## 1๏ธโƒฃ ํŒŒ์ผ ์ „์†ก (Orange Pi๋กœ) + +```bash +# Windows PowerShell ๋˜๋Š” Linux/Mac ํ„ฐ๋ฏธ๋„์—์„œ +scp asset_pilot_docker.tar.gz orangepi@192.168.1.100:~/ +``` + +--- + +## 2๏ธโƒฃ Orange Pi์—์„œ ์„ค์น˜ + +```bash +# SSH ์ ‘์† +ssh orangepi@192.168.1.100 + +# ์••์ถ• ํ•ด์ œ +tar -xzf asset_pilot_docker.tar.gz +cd asset_pilot_docker + +# ์ž๋™ ์„ค์น˜ (Docker๊ฐ€ ์—†์œผ๋ฉด ์ž๋™์œผ๋กœ ์„ค์น˜ํ•จ) +bash start.sh +``` + +**์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ:** +- โœ… Docker ์„ค์น˜ ํ™•์ธ ๋ฐ ์„ค์น˜ +- โœ… ๋น„๋ฐ€๋ฒˆํ˜ธ ์„ค์ • +- โœ… ์ปจํ…Œ์ด๋„ˆ ๋นŒ๋“œ ๋ฐ ์‹คํ–‰ +- โœ… ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™” + +--- + +## 3๏ธโƒฃ ์ ‘์† + +``` +http://[Orange_Pi_IP]:8000 +``` + +์˜ˆ: `http://192.168.1.100:8000` + +--- + +## ๐Ÿ“ฆ Docker ์ปจํ…Œ์ด๋„ˆ ๊ตฌ์กฐ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Orange Pi 5 Plus (Ubuntu) โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Docker Network โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ PostgreSQL โ”‚ โ”‚ App โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ Container โ”‚ โ”‚ Container โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ Port 5432 โ”‚โ†โ†’โ”‚ FastAPI โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Port 8000 โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ”‚ โ†“ โ†“ โ”‚ โ”‚ +โ”‚ โ”‚ [Volume] [Volume] โ”‚ โ”‚ +โ”‚ โ”‚ postgres_data app_logs โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ†‘ โ”‚ +โ”‚ Port 8000 (์™ธ๋ถ€ ์ ‘๊ทผ) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## ๐Ÿ”ง ์ฃผ์š” ๋ช…๋ น์–ด + +### ์ปจํ…Œ์ด๋„ˆ ๊ด€๋ฆฌ +```bash +# ์ „์ฒด ์‹œ์ž‘ +docker compose up -d + +# ์ „์ฒด ์ค‘์ง€ +docker compose down + +# ์ƒํƒœ ํ™•์ธ +docker compose ps + +# ๋กœ๊ทธ ๋ณด๊ธฐ (์‹ค์‹œ๊ฐ„) +docker compose logs -f +``` + +### ํŠน์ • ์ปจํ…Œ์ด๋„ˆ๋งŒ ์ œ์–ด +```bash +# ์•ฑ๋งŒ ์žฌ์‹œ์ž‘ +docker compose restart app + +# DB๋งŒ ์žฌ์‹œ์ž‘ +docker compose restart postgres + +# ์•ฑ ๋กœ๊ทธ๋งŒ ๋ณด๊ธฐ +docker compose logs -f app +``` + +--- + +## ๐Ÿ’พ Windows ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ + +๊ธฐ์กด `user_assets.csv` ํŒŒ์ผ์ด ์žˆ๋‹ค๋ฉด: + +```bash +# 1. CSV ํŒŒ์ผ์„ ์ปจํ…Œ์ด๋„ˆ๋กœ ๋ณต์‚ฌ +docker cp user_assets.csv asset_pilot_app:/app/ + +# 2. ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํ–‰ +docker compose exec app python import_csv.py user_assets.csv +``` + +--- + +## ๐Ÿ”„ ๋ฐฑ์—… ๋ฐ ๋ณต์› + +### ๋ฐฑ์—… ์ƒ์„ฑ +```bash +# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐฑ์—… +docker compose exec postgres pg_dump -U asset_user asset_pilot > backup_$(date +%Y%m%d).sql +``` + +### ๋ฐฑ์—… ๋ณต์› +```bash +# ๋ฐฑ์—… ํŒŒ์ผ ๋ณต์› +cat backup_20260210.sql | docker compose exec -T postgres psql -U asset_user -d asset_pilot +``` + +--- + +## ๐Ÿ› ๏ธ ๋ฌธ์ œ ํ•ด๊ฒฐ + +### ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์‹œ์ž‘๋˜์ง€ ์•Š์Œ +```bash +# ๋กœ๊ทธ ํ™•์ธ +docker compose logs + +# ํŠน์ • ์„œ๋น„์Šค ๋กœ๊ทธ +docker compose logs app +docker compose logs postgres +``` + +### Docker๊ฐ€ ์„ค์น˜๋˜์ง€ ์•Š์Œ +```bash +# Docker ์ž๋™ ์„ค์น˜ +curl -fsSL https://get.docker.com -o get-docker.sh +sudo sh get-docker.sh +sudo usermod -aG docker $USER +newgrp docker # ๋˜๋Š” ๋กœ๊ทธ์•„์›ƒ ํ›„ ์žฌ๋กœ๊ทธ์ธ +``` + +### ํฌํŠธ๊ฐ€ ์ด๋ฏธ ์‚ฌ์šฉ ์ค‘ +```bash +# docker-compose.yml ํŒŒ์ผ ํŽธ์ง‘ +nano docker-compose.yml + +# ports ์„น์…˜ ์ˆ˜์ • +# "8001:8000" # 8000 ๋Œ€์‹  8001 ์‚ฌ์šฉ +``` + +--- + +## ๐Ÿ—‘๏ธ ์™„์ „ ์‚ญ์ œ + +```bash +# ์ปจํ…Œ์ด๋„ˆ + ๋ณผ๋ฅจ ๋ชจ๋‘ ์‚ญ์ œ +docker compose down -v + +# ์ด๋ฏธ์ง€๋„ ์‚ญ์ œ +docker rmi asset_pilot_docker-app postgres:16-alpine +``` + +--- + +## ๐Ÿ“ฑ ๋ชจ๋ฐ”์ผ/ํƒœ๋ธ”๋ฆฟ ์ ‘์† + +1. ๊ฐ™์€ Wi-Fi์— ์—ฐ๊ฒฐ +2. ๋ธŒ๋ผ์šฐ์ €์—์„œ `http://[IP]:8000` ์ ‘์† +3. **ํ™ˆ ํ™”๋ฉด์— ์ถ”๊ฐ€** โ†’ ์•ฑ์ฒ˜๋Ÿผ ์‚ฌ์šฉ! + +--- + +## ๐Ÿ” ๋ณด์•ˆ ํŒ + +```bash +# .env ํŒŒ์ผ ๊ถŒํ•œ ์„ค์ • +chmod 600 .env + +# ๋ฐฉํ™”๋ฒฝ ์„ค์ • +sudo ufw allow 8000/tcp +sudo ufw enable + +# PostgreSQL ์™ธ๋ถ€ ์ ‘๊ทผ ์ฐจ๋‹จ (๊ธฐ๋ณธ๊ฐ’ ์œ ์ง€) +# docker-compose.yml์—์„œ 5432 ํฌํŠธ๋ฅผ 127.0.0.1:5432:5432๋กœ ๋ณ€๊ฒฝ +``` + +--- + +## โœ… ์„ค์น˜ ํ™•์ธ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +- [ ] Docker ์„ค์น˜๋จ (`docker --version`) +- [ ] ํ”„๋กœ์ ํŠธ ์••์ถ• ํ•ด์ œ๋จ +- [ ] `.env` ํŒŒ์ผ ๋น„๋ฐ€๋ฒˆํ˜ธ ์„ค์ •๋จ +- [ ] `docker compose up -d` ์‹คํ–‰๋จ +- [ ] ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰ ์ค‘ (`docker compose ps`) +- [ ] DB ์ดˆ๊ธฐํ™” ์™„๋ฃŒ +- [ ] ์›น ์ ‘์† ๊ฐ€๋Šฅ (`http://[IP]:8000`) + +--- + +## ๐ŸŽฏ ์žฅ์  ์š”์•ฝ + +| ๊ธฐ๋Šฅ | ์ผ๋ฐ˜ ์„ค์น˜ | Docker ์„ค์น˜ | +|------|----------|------------| +| ์„ค์น˜ ๋ณต์žก๋„ | ์ค‘๊ฐ„ | ๋งค์šฐ ์‰ฌ์›€ | +| ํ™˜๊ฒฝ ๊ฒฉ๋ฆฌ | ์—†์Œ | ์™„๋ฒฝ | +| ๋ฐฑ์—…/๋ณต์› | ์ˆ˜๋™ | ๊ฐ„๋‹จ | +| ์—…๋ฐ์ดํŠธ | ๋ณต์žก | ์‰ฌ์›€ | +| ์ด์‹์„ฑ | ๋‚ฎ์Œ | ๋†’์Œ | +| ๋กค๋ฐฑ | ์–ด๋ ค์›€ | ์‰ฌ์›€ | + +--- + +**๋ชจ๋“  ์ค€๋น„ ์™„๋ฃŒ! ํˆฌ์ž ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์‹œ์ž‘ํ•˜์„ธ์š”! ๐Ÿ’ฐ** + +์ƒ์„ธํ•œ ๋‚ด์šฉ์€ `DOCKER_GUIDE.md` ์ฐธ์กฐ diff --git a/.TemporaryDocument/MIGRATION_GUIDE.md b/.TemporaryDocument/MIGRATION_GUIDE.md new file mode 100644 index 0000000..405bedd --- /dev/null +++ b/.TemporaryDocument/MIGRATION_GUIDE.md @@ -0,0 +1,368 @@ +# Asset Pilot - Windows โ†’ Orange Pi 5 Plus ์ „ํ™˜ ๊ฐ€์ด๋“œ + +## ๐Ÿ“‹ ๊ฐœ์š” + +**๋ชฉ์ **: Windows PyQt5 ๋ฐ์Šคํฌํ†ฑ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ Orange Pi 5 Plus (Ubuntu 24.04) ๊ธฐ๋ฐ˜ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์œผ๋กœ ์ „ํ™˜ + +**์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ**: +- PyQt5 GUI โ†’ FastAPI + HTML/JavaScript ์›น ์ธํ„ฐํŽ˜์ด์Šค +- CSV ํŒŒ์ผ ์ €์žฅ โ†’ PostgreSQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค +- Windows ํ† ์ŠคํŠธ ์•Œ๋ฆผ โ†’ ์›น ํ‘ธ์‹œ ์•Œ๋ฆผ (์„ ํƒ์ ) +- ๋กœ์ปฌ ์‹คํ–‰ โ†’ ๋„คํŠธ์›Œํฌ ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ๊ฐ€๋Šฅ + +--- + +## ๐Ÿ”ง ์‹œ์Šคํ…œ ์š”๊ตฌ์‚ฌํ•ญ + +### ํ•˜๋“œ์›จ์–ด +- Orange Pi 5 Plus (ARM64 ์•„ํ‚คํ…์ฒ˜) +- ์ตœ์†Œ 2GB RAM (4GB ๊ถŒ์žฅ) +- ์ตœ์†Œ 8GB ์ €์žฅ๊ณต๊ฐ„ + +### ์†Œํ”„ํŠธ์›จ์–ด +- Ubuntu 24.04 LTS (Joshua Riek ๋ฒ„์ „) +- PostgreSQL 14+ +- Python 3.10+ +- Nginx (์„ ํƒ์ , ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ์šฉ) + +--- + +## ๐Ÿ“ฆ ์„ค์น˜ ๋‹จ๊ณ„ + +### 1๋‹จ๊ณ„: ์‹œ์Šคํ…œ ํŒจํ‚ค์ง€ ์„ค์น˜ + +```bash +# ์‹œ์Šคํ…œ ์—…๋ฐ์ดํŠธ +sudo apt update && sudo apt upgrade -y + +# PostgreSQL ์„ค์น˜ +sudo apt install -y postgresql postgresql-contrib + +# Python ๋ฐ ๊ฐœ๋ฐœ ๋„๊ตฌ ์„ค์น˜ +sudo apt install -y python3 python3-pip python3-venv python3-dev +sudo apt install -y build-essential libpq-dev + +# Nginx ์„ค์น˜ (์„ ํƒ์ ) +sudo apt install -y nginx +``` + +### 2๋‹จ๊ณ„: PostgreSQL ์„ค์ • + +```bash +# PostgreSQL ์„œ๋น„์Šค ์‹œ์ž‘ +sudo systemctl start postgresql +sudo systemctl enable postgresql + +# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐ ์‚ฌ์šฉ์ž ์ƒ์„ฑ +sudo -u postgres psql << EOF +CREATE DATABASE asset_pilot; +CREATE USER asset_user WITH ENCRYPTED PASSWORD 'your_secure_password'; +GRANT ALL PRIVILEGES ON DATABASE asset_pilot TO asset_user; +\q +EOF +``` + +### 3๋‹จ๊ณ„: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ค์น˜ + +```bash +# ์ž‘์—… ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ +mkdir -p ~/asset_pilot_web +cd ~/asset_pilot_web + +# ์ œ๊ณต๋œ ํŒŒ์ผ ์••์ถ• ํ•ด์ œ +unzip asset_pilot_orangepi.zip +cd asset_pilot_orangepi + +# Python ๊ฐ€์ƒํ™˜๊ฒฝ ์ƒ์„ฑ +python3 -m venv venv +source venv/bin/activate + +# ์˜์กด์„ฑ ํŒจํ‚ค์ง€ ์„ค์น˜ +pip install --upgrade pip +pip install -r requirements.txt +``` + +### 4๋‹จ๊ณ„: ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • + +`.env` ํŒŒ์ผ ์ƒ์„ฑ: + +```bash +cat > .env << 'EOF' +# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • +DATABASE_URL=postgresql://asset_user:your_secure_password@localhost/asset_pilot + +# ์„œ๋ฒ„ ์„ค์ • +APP_HOST=0.0.0.0 +APP_PORT=8000 +DEBUG=False + +# ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ๊ฐ„๊ฒฉ (์ดˆ) +FETCH_INTERVAL=5 +EOF +``` + +### 5๋‹จ๊ณ„: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™” + +```bash +# ๊ฐ€์ƒํ™˜๊ฒฝ ํ™œ์„ฑํ™” ์ƒํƒœ์—์„œ +python init_db.py +``` + +### 6๋‹จ๊ณ„: ์„œ๋น„์Šค ๋“ฑ๋ก (์ž๋™ ์‹คํ–‰) + +```bash +# systemd ์„œ๋น„์Šค ํŒŒ์ผ ๋ณต์‚ฌ +sudo cp asset_pilot.service /etc/systemd/system/ + +# ์„œ๋น„์Šค ํŒŒ์ผ ํŽธ์ง‘ (๊ฒฝ๋กœ ํ™•์ธ) +sudo nano /etc/systemd/system/asset_pilot.service + +# ์„œ๋น„์Šค ํ™œ์„ฑํ™” +sudo systemctl daemon-reload +sudo systemctl enable asset_pilot.service +sudo systemctl start asset_pilot.service + +# ์ƒํƒœ ํ™•์ธ +sudo systemctl status asset_pilot.service +``` + +--- + +## ๐ŸŒ ์›น ์ธํ„ฐํŽ˜์ด์Šค ์ ‘๊ทผ + +### ๋กœ์ปฌ ๋„คํŠธ์›Œํฌ์—์„œ ์ ‘๊ทผ + +1. Orange Pi์˜ IP ์ฃผ์†Œ ํ™•์ธ: + ```bash + ip addr show | grep inet + ``` + +2. ์›น ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ ‘์†: + ``` + http://[Orange_Pi_IP]:8000 + ``` + ์˜ˆ: `http://192.168.1.100:8000` + +### Nginx ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ ์„ค์ • (์„ ํƒ์ ) + +ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” Nginx๋ฅผ ํ†ตํ•œ ์ ‘๊ทผ ๊ถŒ์žฅ: + +```bash +sudo nano /etc/nginx/sites-available/asset_pilot +``` + +๋‹ค์Œ ๋‚ด์šฉ ์ž…๋ ฅ: + +```nginx +server { + listen 80; + server_name your_domain_or_ip; + + location / { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /api/stream { + proxy_pass http://127.0.0.1:8000/api/stream; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_buffering off; + proxy_cache off; + } +} +``` + +ํ™œ์„ฑํ™”: + +```bash +sudo ln -s /etc/nginx/sites-available/asset_pilot /etc/nginx/sites-enabled/ +sudo nginx -t +sudo systemctl restart nginx +``` + +--- + +## ๐Ÿ“Š ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + +### Windows ์•ฑ์—์„œ ๋ฐ์ดํ„ฐ ๋‚ด๋ณด๋‚ด๊ธฐ + +๊ธฐ์กด Windows ์•ฑ์˜ `user_assets.csv` ํŒŒ์ผ์„ Orange Pi๋กœ ๋ณต์‚ฌ: + +```bash +# Windows์—์„œ SCP๋กœ ์ „์†ก (์˜ˆ์‹œ) +scp user_assets.csv orangepi@192.168.1.100:~/asset_pilot_web/ +``` + +### ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ + +```bash +cd ~/asset_pilot_web/asset_pilot_orangepi +source venv/bin/activate +python import_csv.py ../user_assets.csv +``` + +--- + +## ๐Ÿ” ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๊ด€๋ฆฌ + +### ๋กœ๊ทธ ํ™•์ธ + +```bash +# ์‹ค์‹œ๊ฐ„ ๋กœ๊ทธ ๋ณด๊ธฐ +sudo journalctl -u asset_pilot.service -f + +# ์ตœ๊ทผ 100์ค„ ๋กœ๊ทธ +sudo journalctl -u asset_pilot.service -n 100 +``` + +### ์„œ๋น„์Šค ๊ด€๋ฆฌ + +```bash +# ์‹œ์ž‘ +sudo systemctl start asset_pilot.service + +# ์ค‘์ง€ +sudo systemctl stop asset_pilot.service + +# ์žฌ์‹œ์ž‘ +sudo systemctl restart asset_pilot.service + +# ์ƒํƒœ ํ™•์ธ +sudo systemctl status asset_pilot.service +``` + +### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐฑ์—… + +```bash +# ๋ฐฑ์—… ์ƒ์„ฑ +pg_dump -U asset_user -h localhost asset_pilot > backup_$(date +%Y%m%d).sql + +# ๋ณต์› +psql -U asset_user -h localhost asset_pilot < backup_20260210.sql +``` + +--- + +## ๐Ÿ” ๋ณด์•ˆ ๊ถŒ์žฅ์‚ฌํ•ญ + +1. **๋ฐฉํ™”๋ฒฝ ์„ค์ •**: + ```bash + sudo ufw allow 8000/tcp # ๋˜๋Š” Nginx ์‚ฌ์šฉ ์‹œ 80/tcp + sudo ufw enable + ``` + +2. **PostgreSQL ์™ธ๋ถ€ ์ ‘๊ทผ ์ฐจ๋‹จ**: ๊ธฐ๋ณธ ์„ค์ • ์œ ์ง€ (localhost๋งŒ ํ—ˆ์šฉ) + +3. **๊ฐ•๋ ฅํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์‚ฌ์šฉ**: `.env` ํŒŒ์ผ์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ + +4. **HTTPS ์„ค์ •** (์„ ํƒ์ ): + - Let's Encrypt ์ธ์ฆ์„œ ์‚ฌ์šฉ + - Nginx SSL ์„ค์ • + +--- + +## ๐Ÿ†˜ ๋ฌธ์ œ ํ•ด๊ฒฐ + +### ์„œ๋น„์Šค๊ฐ€ ์‹œ์ž‘๋˜์ง€ ์•Š์Œ + +```bash +# ๋กœ๊ทธ ํ™•์ธ +sudo journalctl -u asset_pilot.service -n 50 + +# ์ˆ˜๋™ ์‹คํ–‰์œผ๋กœ ์˜ค๋ฅ˜ ํ™•์ธ +cd ~/asset_pilot_web/asset_pilot_orangepi +source venv/bin/activate +python main.py +``` + +### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์˜ค๋ฅ˜ + +```bash +# PostgreSQL ์ƒํƒœ ํ™•์ธ +sudo systemctl status postgresql + +# ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ +psql -U asset_user -h localhost -d asset_pilot +``` + +### ํฌํŠธ ์ถฉ๋Œ + +```bash +# 8000๋ฒˆ ํฌํŠธ ์‚ฌ์šฉ ์ค‘์ธ ํ”„๋กœ์„ธ์Šค ํ™•์ธ +sudo lsof -i :8000 +``` + +--- + +## ๐Ÿ“ฑ ๋ชจ๋ฐ”์ผ ์ ‘๊ทผ + +### ํ™ˆ ํ™”๋ฉด์— ์ถ”๊ฐ€ (PWA ์Šคํƒ€์ผ) + +1. ๋ชจ๋ฐ”์ผ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ ‘์† +2. ๋ธŒ๋ผ์šฐ์ € ๋ฉ”๋‰ด โ†’ "ํ™ˆ ํ™”๋ฉด์— ์ถ”๊ฐ€" +3. ์•ฑ์ฒ˜๋Ÿผ ์‚ฌ์šฉ ๊ฐ€๋Šฅ + +--- + +## ๐Ÿ”„ ์—…๋ฐ์ดํŠธ ์ ˆ์ฐจ + +```bash +cd ~/asset_pilot_web/asset_pilot_orangepi +source venv/bin/activate + +# ์ตœ์‹  ์ฝ”๋“œ ์ ์šฉ +git pull # ๋˜๋Š” ์ƒˆ ํŒŒ์ผ ๋ณต์‚ฌ + +# ์˜์กด์„ฑ ์—…๋ฐ์ดํŠธ +pip install -r requirements.txt --upgrade + +# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (ํ•„์š”์‹œ) +python migrate_db.py + +# ์„œ๋น„์Šค ์žฌ์‹œ์ž‘ +sudo systemctl restart asset_pilot.service +``` + +--- + +## ๐Ÿ“ž ์ง€์› + +๋ฌธ์ œ ๋ฐœ์ƒ ์‹œ: +1. ๋กœ๊ทธ ํ™•์ธ: `sudo journalctl -u asset_pilot.service -n 100` +2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒํƒœ ํ™•์ธ: `sudo systemctl status postgresql` +3. ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ํ™•์ธ: `ping 8.8.8.8` + +--- + +## ๐ŸŽฏ ์ฃผ์š” ์ฐจ์ด์  ์š”์•ฝ + +| ๊ธฐ๋Šฅ | Windows ์•ฑ | Orange Pi ์›น์•ฑ | +|------|-----------|---------------| +| UI | PyQt5 | HTML/JavaScript | +| ์ €์žฅ์†Œ | CSV ํŒŒ์ผ | PostgreSQL | +| ์ ‘๊ทผ์„ฑ | ๋กœ์ปฌ ์‹คํ–‰ | ๋„คํŠธ์›Œํฌ ์ ‘๊ทผ | +| ์•Œ๋ฆผ | Windows ํ† ์ŠคํŠธ | ๋ธŒ๋ผ์šฐ์ € ์•Œ๋ฆผ | +| ๋ฉ€ํ‹ฐ ๊ธฐ๊ธฐ | ๋ถˆ๊ฐ€ | ๊ฐ€๋Šฅ | +| ์ž๋™ ์‹œ์ž‘ | Windows ์ž‘์—… ์Šค์ผ€์ค„๋Ÿฌ | systemd | + +--- + +## โœ… ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +์„ค์น˜ ์™„๋ฃŒ ํ™•์ธ: +- [ ] PostgreSQL ์„ค์น˜ ๋ฐ ์‹คํ–‰ +- [ ] ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒ์„ฑ +- [ ] Python ๊ฐ€์ƒํ™˜๊ฒฝ ์ƒ์„ฑ +- [ ] ์˜์กด์„ฑ ํŒจํ‚ค์ง€ ์„ค์น˜ +- [ ] ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • +- [ ] ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™” +- [ ] ์„œ๋น„์Šค ๋“ฑ๋ก ๋ฐ ์‹œ์ž‘ +- [ ] ์›น ๋ธŒ๋ผ์šฐ์ € ์ ‘์† ํ™•์ธ +- [ ] ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ๋™์ž‘ ํ™•์ธ +- [ ] ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (์„ ํƒ์ ) + +๋ชจ๋“  ํ•ญ๋ชฉ ์™„๋ฃŒ ์‹œ ์‹œ์Šคํ…œ ์‚ฌ์šฉ ์ค€๋น„ ์™„๋ฃŒ! ๐ŸŽ‰ diff --git a/.TemporaryDocument/QUICKSTART.md b/.TemporaryDocument/QUICKSTART.md new file mode 100644 index 0000000..db88adc --- /dev/null +++ b/.TemporaryDocument/QUICKSTART.md @@ -0,0 +1,126 @@ +# Asset Pilot - Orange Pi ๋น ๋ฅธ ์‹œ์ž‘ ๊ฐ€์ด๋“œ + +## ๐Ÿš€ 5๋ถ„ ์•ˆ์— ์‹œ์ž‘ํ•˜๊ธฐ + +### 1๏ธโƒฃ ํŒŒ์ผ ์ „์†ก + +Orange Pi์— `asset_pilot_orangepi.tar.gz` ํŒŒ์ผ์„ ์ „์†ก: + +```bash +# Windows์—์„œ (PowerShell) +scp asset_pilot_orangepi.tar.gz orangepi@192.168.1.100:~/ + +# Linux/Mac์—์„œ +scp asset_pilot_orangepi.tar.gz orangepi@192.168.1.100:~/ +``` + +### 2๏ธโƒฃ Orange Pi์—์„œ ์„ค์น˜ + +```bash +# SSH ์ ‘์† +ssh orangepi@192.168.1.100 + +# ์••์ถ• ํ•ด์ œ +tar -xzf asset_pilot_orangepi.tar.gz +cd asset_pilot_orangepi + +# ์ž๋™ ์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ +bash install.sh +``` + +์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๋‹ค์Œ์„ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค: +- PostgreSQL ์„ค์น˜ ๋ฐ ์„ค์ • +- Python ๊ฐ€์ƒํ™˜๊ฒฝ ์ƒ์„ฑ +- ํ•„์š”ํ•œ ํŒจํ‚ค์ง€ ์„ค์น˜ +- ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™” +- ์„œ๋น„์Šค ๋“ฑ๋ก + +### 3๏ธโƒฃ ์ ‘์† + +์›น ๋ธŒ๋ผ์šฐ์ €์—์„œ: +``` +http://[Orange_Pi_IP]:8000 +``` + +์˜ˆ: `http://192.168.1.100:8000` + +--- + +## ๐Ÿ“ฑ ์Šค๋งˆํŠธํฐ์—์„œ ์ ‘์† + +1. ๊ฐ™์€ Wi-Fi ๋„คํŠธ์›Œํฌ์— ์—ฐ๊ฒฐ +2. ๋ธŒ๋ผ์šฐ์ €์—์„œ `http://[IP]:8000` ์ ‘์† +3. "ํ™ˆ ํ™”๋ฉด์— ์ถ”๊ฐ€" โ†’ ์•ฑ์ฒ˜๋Ÿผ ์‚ฌ์šฉ! + +--- + +## ๐Ÿ”ง ์ฃผ์š” ๋ช…๋ น์–ด + +```bash +# ์„œ๋น„์Šค ์ƒํƒœ ํ™•์ธ +sudo systemctl status asset_pilot + +# ๋กœ๊ทธ ์‹ค์‹œ๊ฐ„ ๋ณด๊ธฐ +sudo journalctl -u asset_pilot -f + +# ์„œ๋น„์Šค ์žฌ์‹œ์ž‘ +sudo systemctl restart asset_pilot + +# ์„œ๋น„์Šค ์ค‘์ง€ +sudo systemctl stop asset_pilot +``` + +--- + +## ๐Ÿ“Š Windows ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ + +๊ธฐ์กด `user_assets.csv` ํŒŒ์ผ์ด ์žˆ๋‹ค๋ฉด: + +```bash +# CSV ํŒŒ์ผ์„ Orange Pi๋กœ ๋ณต์‚ฌ +scp user_assets.csv orangepi@192.168.1.100:~/ + +# Orange Pi์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ +cd ~/asset_pilot_orangepi +source venv/bin/activate +python import_csv.py ../user_assets.csv +``` + +--- + +## โ“ ๋ฌธ์ œ ํ•ด๊ฒฐ + +### ์ ‘์†์ด ์•ˆ ๋  ๋•Œ + +1. ์„œ๋น„์Šค ํ™•์ธ: + ```bash + sudo systemctl status asset_pilot + ``` + +2. ๋ฐฉํ™”๋ฒฝ ํ™•์ธ: + ```bash + sudo ufw allow 8000/tcp + ``` + +3. IP ์ฃผ์†Œ ํ™•์ธ: + ```bash + ip addr show + ``` + +### ๋ฐ์ดํ„ฐ๊ฐ€ ์ˆ˜์ง‘๋˜์ง€ ์•Š์„ ๋•Œ + +๋กœ๊ทธ ํ™•์ธ: +```bash +sudo journalctl -u asset_pilot -n 100 +``` + +--- + +## ๐Ÿ“š ๋” ์ž์„ธํ•œ ์ •๋ณด + +- **์ „์ฒด ๊ฐ€์ด๋“œ**: MIGRATION_GUIDE.md +- **ํ”„๋กœ์ ํŠธ ๋ฌธ์„œ**: README.md (ํ”„๋กœ์ ํŠธ ๋‚ด) + +--- + +**์ค€๋น„ ์™„๋ฃŒ! ์ด์ œ ์ž์‚ฐ์„ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜์„ธ์š”! ๐Ÿ’ฐ** diff --git a/.TemporaryDocument/asset_pilot_docker.tar.gz b/.TemporaryDocument/asset_pilot_docker.tar.gz new file mode 100644 index 0000000..2ab56e7 Binary files /dev/null and b/.TemporaryDocument/asset_pilot_docker.tar.gz differ diff --git a/.TemporaryDocument/asset_pilot_orangepi.tar.gz b/.TemporaryDocument/asset_pilot_orangepi.tar.gz new file mode 100644 index 0000000..7fa3a8b Binary files /dev/null and b/.TemporaryDocument/asset_pilot_orangepi.tar.gz differ diff --git a/.TemporaryDocument/assetpilot.ico b/.TemporaryDocument/assetpilot.ico new file mode 100644 index 0000000..e3a3172 Binary files /dev/null and b/.TemporaryDocument/assetpilot.ico differ diff --git a/.TemporaryDocument/claude.md b/.TemporaryDocument/claude.md new file mode 100644 index 0000000..46e1187 --- /dev/null +++ b/.TemporaryDocument/claude.md @@ -0,0 +1,130 @@ +์ด๊ฑธ postgresql ๋„์ปค ๋”ฐ๋กœ ๊ฐœ๋ฐœํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ๋„์ปค ๋”ฐ๋กœ ์ด๋ ‡๊ฒŒ ์„ค์น˜ํ•˜๊ณ  ์‹ถ์€๋ฐ ๊ณผ์ •์•Œ๋ ค์ค˜ + +์˜ค์ „ 8:10 + +4๊ฐœ์˜ ํŒŒ์ผ ์ƒ์„ฑ๋จ, ๋ช…๋ น 4๊ฐœ ์‹คํ–‰ํ•จ +์ด์ œ ๋‹ค์‹œ ํŒŒ์ผ๋“ค์„ ์ƒ์„ฑํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. + + +๋ช…๋ น 3๊ฐœ ์‹คํ–‰ํ•จ +์ข‹์Šต๋‹ˆ๋‹ค! ์ด์ œ Docker ๊ด€๋ จ ํŒŒ์ผ๋“ค์„ ์ถ”๊ฐ€ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. + + +9๊ฐœ์˜ ํŒŒ์ผ ์ƒ์„ฑ๋จ, ๋ช…๋ น ์‹คํ–‰ํ•จ +์ด์ œ ํ”„๋กœ์ ํŠธ๋ฅผ ์••์ถ•ํ•˜์—ฌ ์ œ๊ณตํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. + + +๋ช…๋ น 2๊ฐœ ์‹คํ–‰ํ•จ, ํŒŒ์ผ ์ƒ์„ฑ๋จ +์™„๋ฒฝํ•ฉ๋‹ˆ๋‹ค! ์ด์ œ ๋ชจ๋“  ํŒŒ์ผ์„ ์ œ๊ณตํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. + + +Presented 2 files +์™„๋ฃŒํ–ˆ์Šต๋‹ˆ๋‹ค! ๐Ÿณ + +๐Ÿ“ฆ Docker ๋ฒ„์ „ ์ œ๊ณต ์™„๋ฃŒ +์ œ๊ณต๋œ ํŒŒ์ผ +asset_pilot_docker.tar.gz - Docker ๊ธฐ๋ฐ˜ ์ „์ฒด ํŒจํ‚ค์ง€ +DOCKER_QUICKSTART.md - Docker ๋น ๋ฅธ ์‹œ์ž‘ ๊ฐ€์ด๋“œ +๐Ÿณ Docker ์„ค์น˜ ๋ฐฉ์‹์˜ ์žฅ์  +๋…๋ฆฝ๋œ ์ปจํ…Œ์ด๋„ˆ ๊ตฌ์กฐ +PostgreSQL Container (DB) โ†โ†’ App Container (FastAPI) + โ†“ โ†“ +Volume (postgres_data) Volume (app_logs) +์ฃผ์š” ์ด์ : + +โœ… PostgreSQL๊ณผ ์•ฑ์ด ๋ณ„๋„ ์ปจํ…Œ์ด๋„ˆ๋กœ ์™„์ „ ๊ฒฉ๋ฆฌ +โœ… ํ•œ ๋ฒˆ์˜ ๋ช…๋ น์œผ๋กœ ์ „์ฒด ์‹œ์Šคํ…œ ์‹คํ–‰ +โœ… ์ปจํ…Œ์ด๋„ˆ๋ณ„ ๋…๋ฆฝ์  ์žฌ์‹œ์ž‘ ๊ฐ€๋Šฅ +โœ… ๋ณผ๋ฅจ์œผ๋กœ ๋ฐ์ดํ„ฐ ์˜๊ตฌ ์ €์žฅ +โœ… ๋ฐฑ์—…/๋ณต์› ๋งค์šฐ ๊ฐ„๋‹จ +โœ… ์ด์‹์„ฑ ์ตœ๊ณ  (๋‹ค๋ฅธ ์„œ๋ฒ„๋กœ ์‰ฝ๊ฒŒ ์ด๋™) +๐Ÿš€ ์„ค์น˜ ๊ณผ์ • +์ดˆ๊ฐ„๋‹จ ์„ค์น˜ (3๋‹จ๊ณ„) +bash +# 1. ์••์ถ• ํ•ด์ œ +tar -xzf asset_pilot_docker.tar.gz +cd asset_pilot_docker + +# 2. ์ž๋™ ์„ค์น˜ ์‹คํ–‰ +bash start.sh + +# 3. ์™„๋ฃŒ! ์ ‘์† +# http://[IP]:8000 +start.sh ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์ž๋™์œผ๋กœ: +Docker ์„ค์น˜ ํ™•์ธ (์—†์œผ๋ฉด ์ž๋™ ์„ค์น˜) +Docker Compose ํ™•์ธ (์—†์œผ๋ฉด ์ž๋™ ์„ค์น˜) +๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ๋ฐ›์•„ .env ํŒŒ์ผ ์ƒ์„ฑ +PostgreSQL ์ปจํ…Œ์ด๋„ˆ ์‹œ์ž‘ +App ์ปจํ…Œ์ด๋„ˆ ๋นŒ๋“œ ๋ฐ ์‹œ์ž‘ +๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ž๋™ ์ดˆ๊ธฐํ™” +์ ‘์† URL ์•ˆ๋‚ด +๐Ÿ”ง ์ฃผ์š” ๊ด€๋ฆฌ ๋ช…๋ น์–ด +์ปจํ…Œ์ด๋„ˆ ๊ด€๋ฆฌ +bash +docker compose up -d # ์‹œ์ž‘ (๋ฐฑ๊ทธ๋ผ์šด๋“œ) +docker compose down # ์ค‘์ง€ +docker compose ps # ์ƒํƒœ ํ™•์ธ +docker compose logs -f # ๋กœ๊ทธ ์‹ค์‹œ๊ฐ„ +docker compose restart app # ์•ฑ๋งŒ ์žฌ์‹œ์ž‘ +๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ +bash +# ๋ฐฑ์—… +docker compose exec postgres pg_dump -U asset_user asset_pilot > backup.sql + +# ๋ณต์› +cat backup.sql | docker compose exec -T postgres psql -U asset_user -d asset_pilot + +# CSV ๊ฐ€์ ธ์˜ค๊ธฐ +docker cp user_assets.csv asset_pilot_app:/app/ +docker compose exec app python import_csv.py user_assets.csv +๐Ÿ“Š ์ปจํ…Œ์ด๋„ˆ ๊ตฌ์„ฑ ์ƒ์„ธ +PostgreSQL ์ปจํ…Œ์ด๋„ˆ +์ด๋ฏธ์ง€: postgres:16-alpine (๊ฒฝ๋Ÿ‰) +ํฌํŠธ: 5432 (๋‚ด๋ถ€ ๋„คํŠธ์›Œํฌ๋งŒ) +๋ณผ๋ฅจ: postgres_data (๋ฐ์ดํ„ฐ ์˜๊ตฌ ์ €์žฅ) +ํ—ฌ์Šค์ฒดํฌ: ์ž๋™ ์ƒํƒœ ํ™•์ธ +App ์ปจํ…Œ์ด๋„ˆ +์ด๋ฏธ์ง€: ์ž์ฒด ๋นŒ๋“œ (Python 3.11) +ํฌํŠธ: 8000 (์™ธ๋ถ€ ์ ‘๊ทผ ๊ฐ€๋Šฅ) +์˜์กด์„ฑ: PostgreSQL ์ค€๋น„ ์™„๋ฃŒ ํ›„ ์‹œ์ž‘ +ํ—ฌ์Šค์ฒดํฌ: /health ์—”๋“œํฌ์ธํŠธ ํ™•์ธ +๐Ÿ”„ ์—…๋ฐ์ดํŠธ ๋ฐฉ๋ฒ• +bash +# ์ฝ”๋“œ ์—…๋ฐ์ดํŠธ ํ›„ +docker compose build app # ์ด๋ฏธ์ง€ ์žฌ๋นŒ๋“œ +docker compose up -d app # ์•ฑ๋งŒ ์žฌ์‹œ์ž‘ +๐Ÿ” ๋ณด์•ˆ ์„ค์ • +PostgreSQL: ๋‚ด๋ถ€ ๋„คํŠธ์›Œํฌ๋งŒ ์ ‘๊ทผ +.env ํŒŒ์ผ: ๊ถŒํ•œ 600 (์†Œ์œ ์ž๋งŒ ์ฝ๊ธฐ/์“ฐ๊ธฐ) +๋ฐฉํ™”๋ฒฝ: 8000๋ฒˆ ํฌํŠธ๋งŒ ๊ฐœ๋ฐฉ +๋น„๋ฃจํŠธ ์‚ฌ์šฉ์ž: ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€์—์„œ appuser๋กœ ์‹คํ–‰ +๐Ÿ’ก ์ผ๋ฐ˜ ์„ค์น˜ vs Docker ์„ค์น˜ +ํ•ญ๋ชฉ ์ผ๋ฐ˜ ์„ค์น˜ Docker ์„ค์น˜ +์„ค์น˜ ๋‚œ์ด๋„ ์ค‘๊ฐ„ (์—ฌ๋Ÿฌ ๋‹จ๊ณ„) ์‰ฌ์›€ (3๋‹จ๊ณ„) +์˜์กด์„ฑ ๊ด€๋ฆฌ ์ˆ˜๋™ ์„ค์น˜ ํ•„์š” ์ž๋™ ์ฒ˜๋ฆฌ +ํ™˜๊ฒฝ ๊ฒฉ๋ฆฌ ์—†์Œ ์™„๋ฒฝ +๋ฐฑ์—… ์ˆ˜๋™ (๋ณต์žก) ๊ฐ„๋‹จ +์—…๋ฐ์ดํŠธ ๋ณต์žก ์‰ฌ์›€ +๋กค๋ฐฑ ์–ด๋ ค์›€ ์‰ฌ์›€ +์ด์‹์„ฑ ๋‚ฎ์Œ ๋†’์Œ +๋ฆฌ์†Œ์Šค ๊ฐ€๋ฒผ์›€ ์•ฝ๊ฐ„ ๋” ์‚ฌ์šฉ +๐Ÿ“š ํฌํ•จ๋œ ๋ฌธ์„œ +ํ”„๋กœ์ ํŠธ ๋‚ด๋ถ€ (์••์ถ• ํ•ด์ œ ํ›„): + +README.md - ํ”„๋กœ์ ํŠธ ๊ฐœ์š” +DOCKER_GUIDE.md - ์ƒ์„ธ ์„ค์น˜/๊ด€๋ฆฌ ๊ฐ€์ด๋“œ +docker-compose.yml - ์ปจํ…Œ์ด๋„ˆ ์„ค์ • +Dockerfile - ์•ฑ ์ด๋ฏธ์ง€ ๋นŒ๋“œ ์„ค์ • +start.sh - ์ž๋™ ์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ +๐ŸŽฏ ๊ถŒ์žฅ ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค +Docker ์„ค์น˜ ์ถ”์ฒœ: + +โœ… ๊น”๋”ํ•œ ํ™˜๊ฒฝ ์›ํ•  ๋•Œ +โœ… ์‰ฌ์šด ๋ฐฑ์—…/๋ณต์› ํ•„์š”ํ•  ๋•Œ +โœ… ์—ฌ๋Ÿฌ ์„œ๋ฒ„์— ๋ฐฐํฌํ•  ๋•Œ +โœ… ์ปจํ…Œ์ด๋„ˆ ๊ธฐ์ˆ  ์„ ํ˜ธํ•  ๋•Œ +์ผ๋ฐ˜ ์„ค์น˜ ์ถ”์ฒœ: + +โœ… Docker ์—†๋Š” ํ™˜๊ฒฝ +โœ… ์ตœ์†Œ ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ +โœ… ์ง์ ‘ ์ œ์–ด ์„ ํ˜ธ \ No newline at end of file diff --git a/.TemporaryDocument/ํ•„์š” Linux๋ช…๋ น๋“ค ๋ชจ์Œ.md b/.TemporaryDocument/ํ•„์š” Linux๋ช…๋ น๋“ค ๋ชจ์Œ.md new file mode 100644 index 0000000..d7aa653 --- /dev/null +++ b/.TemporaryDocument/ํ•„์š” Linux๋ช…๋ น๋“ค ๋ชจ์Œ.md @@ -0,0 +1,21 @@ +# ํ”„๋กœ๊ทธ๋žจ ์ˆ˜์ •ํ›„ +docker compose up -d --build + +# ์‹คํ–‰์„ ํ„ฐ๋ฏธ๋„ ์—์„œ ๋ณด๊ณ  ์‹ถ์œผ๋ฉด +docker compose logs -f app + + +# AssetPilot์˜ ์˜ค๋ Œ์ง€ ํŒŒ์ด ๋ฆฌ์†Œ์Šค ๋ถ€ํ•˜ ํ™•์ธํ•˜๊ณ  ์‹ถ์œผ๋ฉด +docker stats asset_pilot_app asset_pilot_db + +# docker compose down ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ +๋ณดํ†ต์˜ ์ฝ”๋“œ ์ˆ˜์ •(Python ํŒŒ์ผ ์ˆ˜์ • ๋“ฑ)์—๋Š” up --build๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํ•˜์ง€๋งŒ, ์•„๋ž˜ ์ƒํ™ฉ์—์„œ๋Š” down์„ ๋จผ์ € ํ•˜๋Š” ๊ฒƒ์ด ๊น”๋”ํ•ฉ๋‹ˆ๋‹ค. + +ํฌํŠธ ๋ณ€๊ฒฝ: docker-compose.yml์—์„œ ports ์„ค์ •์„ ๋ฐ”๊ฟจ์„ ๋•Œ. + +๋„คํŠธ์›Œํฌ ๊ตฌ์กฐ ๋ณ€๊ฒฝ: ์„œ๋น„์Šค ๊ฐ„์˜ ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ๋ฐฉ์‹์„ ํฌ๊ฒŒ ์ˆ˜์ •ํ–ˆ์„ ๋•Œ. + +์™„์ „ ์ดˆ๊ธฐํ™”: volumes์— ์Œ“์ธ ๋ฐ์ดํ„ฐ๊นŒ์ง€ ์‹น ์ง€์šฐ๊ณ  ์ƒˆ๋กœ ์‹œ์ž‘ํ•˜๊ณ  ์‹ถ์„ ๋•Œ (์ด๋•Œ๋Š” docker compose down -v). + +์ข€๋น„ ์ปจํ…Œ์ด๋„ˆ: ๊ฐ€๋” ๋„์ปค ์—”์ง„์˜ ๋ฒ„๊ทธ๋กœ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๊ต์ฒด๋˜์ง€ ์•Š๊ณ  ๊ผฌ์—ฌ์žˆ์„ ๋•Œ. + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7fbbdf2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.py[cod] +postgres_data/ # DB ๋ฐ์ดํ„ฐ๋Š” ๋”ฐ๋กœ ๊ด€๋ฆฌ +.env # ๋น„๋ฐ€๋ฒˆํ˜ธ ๋“ฑ ๋ฏผ๊ฐ ์ •๋ณด \ No newline at end of file diff --git a/asset_pilot_docker/.gitignore b/asset_pilot_docker/.gitignore new file mode 100644 index 0000000..b0943a2 --- /dev/null +++ b/asset_pilot_docker/.gitignore @@ -0,0 +1,59 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +venv/ +env/ +ENV/ + +# ํ™˜๊ฒฝ ๋ณ€์ˆ˜ +.env +.env.local + +# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค +*.db +*.sqlite +*.sqlite3 + +# ๋กœ๊ทธ +*.log +logs/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# CSV ๋ฐ์ดํ„ฐ +*.csv +!example.csv + +# ๋ฐฑ์—… ํŒŒ์ผ +*.sql +backup/ + +# Docker +.dockerignore diff --git a/asset_pilot_docker/.venv/bin/Activate.ps1 b/asset_pilot_docker/.venv/bin/Activate.ps1 new file mode 100644 index 0000000..b49d77b --- /dev/null +++ b/asset_pilot_docker/.venv/bin/Activate.ps1 @@ -0,0 +1,247 @@ +<# +.Synopsis +Activate a Python virtual environment for the current PowerShell session. + +.Description +Pushes the python executable for a virtual environment to the front of the +$Env:PATH environment variable and sets the prompt to signify that you are +in a Python virtual environment. Makes use of the command line switches as +well as the `pyvenv.cfg` file values present in the virtual environment. + +.Parameter VenvDir +Path to the directory that contains the virtual environment to activate. The +default value for this is the parent of the directory that the Activate.ps1 +script is located within. + +.Parameter Prompt +The prompt prefix to display when this virtual environment is activated. By +default, this prompt is the name of the virtual environment folder (VenvDir) +surrounded by parentheses and followed by a single space (ie. '(.venv) '). + +.Example +Activate.ps1 +Activates the Python virtual environment that contains the Activate.ps1 script. + +.Example +Activate.ps1 -Verbose +Activates the Python virtual environment that contains the Activate.ps1 script, +and shows extra information about the activation as it executes. + +.Example +Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv +Activates the Python virtual environment located in the specified location. + +.Example +Activate.ps1 -Prompt "MyPython" +Activates the Python virtual environment that contains the Activate.ps1 script, +and prefixes the current prompt with the specified string (surrounded in +parentheses) while the virtual environment is active. + +.Notes +On Windows, it may be required to enable this Activate.ps1 script by setting the +execution policy for the user. You can do this by issuing the following PowerShell +command: + +PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + +For more information on Execution Policies: +https://go.microsoft.com/fwlink/?LinkID=135170 + +#> +Param( + [Parameter(Mandatory = $false)] + [String] + $VenvDir, + [Parameter(Mandatory = $false)] + [String] + $Prompt +) + +<# Function declarations --------------------------------------------------- #> + +<# +.Synopsis +Remove all shell session elements added by the Activate script, including the +addition of the virtual environment's Python executable from the beginning of +the PATH variable. + +.Parameter NonDestructive +If present, do not remove this function from the global namespace for the +session. + +#> +function global:deactivate ([switch]$NonDestructive) { + # Revert to original values + + # The prior prompt: + if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { + Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt + Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT + } + + # The prior PYTHONHOME: + if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { + Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME + Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME + } + + # The prior PATH: + if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { + Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH + Remove-Item -Path Env:_OLD_VIRTUAL_PATH + } + + # Just remove the VIRTUAL_ENV altogether: + if (Test-Path -Path Env:VIRTUAL_ENV) { + Remove-Item -Path env:VIRTUAL_ENV + } + + # Just remove VIRTUAL_ENV_PROMPT altogether. + if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { + Remove-Item -Path env:VIRTUAL_ENV_PROMPT + } + + # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: + if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { + Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force + } + + # Leave deactivate function in the global namespace if requested: + if (-not $NonDestructive) { + Remove-Item -Path function:deactivate + } +} + +<# +.Description +Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the +given folder, and returns them in a map. + +For each line in the pyvenv.cfg file, if that line can be parsed into exactly +two strings separated by `=` (with any amount of whitespace surrounding the =) +then it is considered a `key = value` line. The left hand string is the key, +the right hand is the value. + +If the value starts with a `'` or a `"` then the first and last character is +stripped from the value before being captured. + +.Parameter ConfigDir +Path to the directory that contains the `pyvenv.cfg` file. +#> +function Get-PyVenvConfig( + [String] + $ConfigDir +) { + Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" + + # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). + $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue + + # An empty map will be returned if no config file is found. + $pyvenvConfig = @{ } + + if ($pyvenvConfigPath) { + + Write-Verbose "File exists, parse `key = value` lines" + $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath + + $pyvenvConfigContent | ForEach-Object { + $keyval = $PSItem -split "\s*=\s*", 2 + if ($keyval[0] -and $keyval[1]) { + $val = $keyval[1] + + # Remove extraneous quotations around a string value. + if ("'""".Contains($val.Substring(0, 1))) { + $val = $val.Substring(1, $val.Length - 2) + } + + $pyvenvConfig[$keyval[0]] = $val + Write-Verbose "Adding Key: '$($keyval[0])'='$val'" + } + } + } + return $pyvenvConfig +} + + +<# Begin Activate script --------------------------------------------------- #> + +# Determine the containing directory of this script +$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$VenvExecDir = Get-Item -Path $VenvExecPath + +Write-Verbose "Activation script is located in path: '$VenvExecPath'" +Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" +Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" + +# Set values required in priority: CmdLine, ConfigFile, Default +# First, get the location of the virtual environment, it might not be +# VenvExecDir if specified on the command line. +if ($VenvDir) { + Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" +} +else { + Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." + $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") + Write-Verbose "VenvDir=$VenvDir" +} + +# Next, read the `pyvenv.cfg` file to determine any required value such +# as `prompt`. +$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir + +# Next, set the prompt from the command line, or the config file, or +# just use the name of the virtual environment folder. +if ($Prompt) { + Write-Verbose "Prompt specified as argument, using '$Prompt'" +} +else { + Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" + if ($pyvenvCfg -and $pyvenvCfg['prompt']) { + Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" + $Prompt = $pyvenvCfg['prompt']; + } + else { + Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" + Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" + $Prompt = Split-Path -Path $venvDir -Leaf + } +} + +Write-Verbose "Prompt = '$Prompt'" +Write-Verbose "VenvDir='$VenvDir'" + +# Deactivate any currently active virtual environment, but leave the +# deactivate function in place. +deactivate -nondestructive + +# Now set the environment variable VIRTUAL_ENV, used by many tools to determine +# that there is an activated venv. +$env:VIRTUAL_ENV = $VenvDir + +if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { + + Write-Verbose "Setting prompt to '$Prompt'" + + # Set the prompt to include the env name + # Make sure _OLD_VIRTUAL_PROMPT is global + function global:_OLD_VIRTUAL_PROMPT { "" } + Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT + New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt + + function global:prompt { + Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " + _OLD_VIRTUAL_PROMPT + } + $env:VIRTUAL_ENV_PROMPT = $Prompt +} + +# Clear PYTHONHOME +if (Test-Path -Path Env:PYTHONHOME) { + Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME + Remove-Item -Path Env:PYTHONHOME +} + +# Add the venv to the PATH +Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH +$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/asset_pilot_docker/.venv/bin/activate b/asset_pilot_docker/.venv/bin/activate new file mode 100644 index 0000000..6d11f77 --- /dev/null +++ b/asset_pilot_docker/.venv/bin/activate @@ -0,0 +1,70 @@ +# This file must be used with "source bin/activate" *from bash* +# You cannot run it directly + +deactivate () { + # reset old environment variables + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + + # Call hash to forget past commands. Without forgetting + # past commands the $PATH changes we made may not be respected + hash -r 2> /dev/null + + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + # Self destruct! + unset -f deactivate + fi +} + +# unset irrelevant variables +deactivate nondestructive + +# on Windows, a path can contain colons and backslashes and has to be converted: +if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then + # transform D:\path\to\venv to /d/path/to/venv on MSYS + # and to /cygdrive/d/path/to/venv on Cygwin + export VIRTUAL_ENV=$(cygpath /home/ubuntu/AssetPilot/asset_pilot_docker/.venv) +else + # use the path as-is + export VIRTUAL_ENV=/home/ubuntu/AssetPilot/asset_pilot_docker/.venv +fi + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/"bin":$PATH" +export PATH + +# unset PYTHONHOME if set +# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) +# could use `if (set -u; : $PYTHONHOME) ;` in bash +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" + unset PYTHONHOME +fi + +if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then + _OLD_VIRTUAL_PS1="${PS1:-}" + PS1='(.venv) '"${PS1:-}" + export PS1 + VIRTUAL_ENV_PROMPT='(.venv) ' + export VIRTUAL_ENV_PROMPT +fi + +# Call hash to forget past commands. Without forgetting +# past commands the $PATH changes we made may not be respected +hash -r 2> /dev/null diff --git a/asset_pilot_docker/.venv/bin/activate.csh b/asset_pilot_docker/.venv/bin/activate.csh new file mode 100644 index 0000000..bf55dc8 --- /dev/null +++ b/asset_pilot_docker/.venv/bin/activate.csh @@ -0,0 +1,27 @@ +# This file must be used with "source bin/activate.csh" *from csh*. +# You cannot run it directly. + +# Created by Davide Di Blasi . +# Ported to Python 3.3 venv by Andrew Svetlov + +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' + +# Unset irrelevant variables. +deactivate nondestructive + +setenv VIRTUAL_ENV /home/ubuntu/AssetPilot/asset_pilot_docker/.venv + +set _OLD_VIRTUAL_PATH="$PATH" +setenv PATH "$VIRTUAL_ENV/"bin":$PATH" + + +set _OLD_VIRTUAL_PROMPT="$prompt" + +if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then + set prompt = '(.venv) '"$prompt" + setenv VIRTUAL_ENV_PROMPT '(.venv) ' +endif + +alias pydoc python -m pydoc + +rehash diff --git a/asset_pilot_docker/.venv/bin/activate.fish b/asset_pilot_docker/.venv/bin/activate.fish new file mode 100644 index 0000000..b987b26 --- /dev/null +++ b/asset_pilot_docker/.venv/bin/activate.fish @@ -0,0 +1,69 @@ +# This file must be used with "source /bin/activate.fish" *from fish* +# (https://fishshell.com/). You cannot run it directly. + +function deactivate -d "Exit virtual environment and return to normal shell environment" + # reset old environment variables + if test -n "$_OLD_VIRTUAL_PATH" + set -gx PATH $_OLD_VIRTUAL_PATH + set -e _OLD_VIRTUAL_PATH + end + if test -n "$_OLD_VIRTUAL_PYTHONHOME" + set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -e _OLD_VIRTUAL_PYTHONHOME + end + + if test -n "$_OLD_FISH_PROMPT_OVERRIDE" + set -e _OLD_FISH_PROMPT_OVERRIDE + # prevents error when using nested fish instances (Issue #93858) + if functions -q _old_fish_prompt + functions -e fish_prompt + functions -c _old_fish_prompt fish_prompt + functions -e _old_fish_prompt + end + end + + set -e VIRTUAL_ENV + set -e VIRTUAL_ENV_PROMPT + if test "$argv[1]" != "nondestructive" + # Self-destruct! + functions -e deactivate + end +end + +# Unset irrelevant variables. +deactivate nondestructive + +set -gx VIRTUAL_ENV /home/ubuntu/AssetPilot/asset_pilot_docker/.venv + +set -gx _OLD_VIRTUAL_PATH $PATH +set -gx PATH "$VIRTUAL_ENV/"bin $PATH + +# Unset PYTHONHOME if set. +if set -q PYTHONHOME + set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME + set -e PYTHONHOME +end + +if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" + # fish uses a function instead of an env var to generate the prompt. + + # Save the current fish_prompt function as the function _old_fish_prompt. + functions -c fish_prompt _old_fish_prompt + + # With the original prompt function renamed, we can override with our own. + function fish_prompt + # Save the return status of the last command. + set -l old_status $status + + # Output the venv prompt; color taken from the blue of the Python logo. + printf "%s%s%s" (set_color 4B8BBE) '(.venv) ' (set_color normal) + + # Restore the return status of the previous command. + echo "exit $old_status" | . + # Output the original/"old" prompt. + _old_fish_prompt + end + + set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" + set -gx VIRTUAL_ENV_PROMPT '(.venv) ' +end diff --git a/asset_pilot_docker/.venv/bin/dotenv b/asset_pilot_docker/.venv/bin/dotenv new file mode 100755 index 0000000..f3b29bb --- /dev/null +++ b/asset_pilot_docker/.venv/bin/dotenv @@ -0,0 +1,8 @@ +#!/home/ubuntu/AssetPilot/asset_pilot_docker/.venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from dotenv.__main__ import cli +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli()) diff --git a/asset_pilot_docker/.venv/bin/normalizer b/asset_pilot_docker/.venv/bin/normalizer new file mode 100755 index 0000000..88d04e9 --- /dev/null +++ b/asset_pilot_docker/.venv/bin/normalizer @@ -0,0 +1,8 @@ +#!/home/ubuntu/AssetPilot/asset_pilot_docker/.venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from charset_normalizer.cli import cli_detect +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli_detect()) diff --git a/asset_pilot_docker/.venv/bin/pip b/asset_pilot_docker/.venv/bin/pip new file mode 100755 index 0000000..84f91bc --- /dev/null +++ b/asset_pilot_docker/.venv/bin/pip @@ -0,0 +1,8 @@ +#!/home/ubuntu/AssetPilot/asset_pilot_docker/.venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/asset_pilot_docker/.venv/bin/pip3 b/asset_pilot_docker/.venv/bin/pip3 new file mode 100755 index 0000000..84f91bc --- /dev/null +++ b/asset_pilot_docker/.venv/bin/pip3 @@ -0,0 +1,8 @@ +#!/home/ubuntu/AssetPilot/asset_pilot_docker/.venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/asset_pilot_docker/.venv/bin/pip3.12 b/asset_pilot_docker/.venv/bin/pip3.12 new file mode 100755 index 0000000..84f91bc --- /dev/null +++ b/asset_pilot_docker/.venv/bin/pip3.12 @@ -0,0 +1,8 @@ +#!/home/ubuntu/AssetPilot/asset_pilot_docker/.venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/asset_pilot_docker/.venv/bin/python b/asset_pilot_docker/.venv/bin/python new file mode 120000 index 0000000..b8a0adb --- /dev/null +++ b/asset_pilot_docker/.venv/bin/python @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/asset_pilot_docker/.venv/bin/python3 b/asset_pilot_docker/.venv/bin/python3 new file mode 120000 index 0000000..ae65fda --- /dev/null +++ b/asset_pilot_docker/.venv/bin/python3 @@ -0,0 +1 @@ +/usr/bin/python3 \ No newline at end of file diff --git a/asset_pilot_docker/.venv/bin/python3.12 b/asset_pilot_docker/.venv/bin/python3.12 new file mode 120000 index 0000000..b8a0adb --- /dev/null +++ b/asset_pilot_docker/.venv/bin/python3.12 @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/asset_pilot_docker/.venv/bin/uvicorn b/asset_pilot_docker/.venv/bin/uvicorn new file mode 100755 index 0000000..c8ccea9 --- /dev/null +++ b/asset_pilot_docker/.venv/bin/uvicorn @@ -0,0 +1,8 @@ +#!/home/ubuntu/AssetPilot/asset_pilot_docker/.venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from uvicorn.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/asset_pilot_docker/.venv/bin/watchfiles b/asset_pilot_docker/.venv/bin/watchfiles new file mode 100755 index 0000000..32f4b4a --- /dev/null +++ b/asset_pilot_docker/.venv/bin/watchfiles @@ -0,0 +1,8 @@ +#!/home/ubuntu/AssetPilot/asset_pilot_docker/.venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from watchfiles.cli import cli +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli()) diff --git a/asset_pilot_docker/.venv/bin/websockets b/asset_pilot_docker/.venv/bin/websockets new file mode 100755 index 0000000..a3fe2bd --- /dev/null +++ b/asset_pilot_docker/.venv/bin/websockets @@ -0,0 +1,8 @@ +#!/home/ubuntu/AssetPilot/asset_pilot_docker/.venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from websockets.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/asset_pilot_docker/.venv/include/site/python3.12/greenlet/greenlet.h b/asset_pilot_docker/.venv/include/site/python3.12/greenlet/greenlet.h new file mode 100644 index 0000000..d02a16e --- /dev/null +++ b/asset_pilot_docker/.venv/include/site/python3.12/greenlet/greenlet.h @@ -0,0 +1,164 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ + +/* Greenlet object interface */ + +#ifndef Py_GREENLETOBJECT_H +#define Py_GREENLETOBJECT_H + + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* This is deprecated and undocumented. It does not change. */ +#define GREENLET_VERSION "1.0.0" + +#ifndef GREENLET_MODULE +#define implementation_ptr_t void* +#endif + +typedef struct _greenlet { + PyObject_HEAD + PyObject* weakreflist; + PyObject* dict; + implementation_ptr_t pimpl; +} PyGreenlet; + +#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type)) + + +/* C API functions */ + +/* Total number of symbols that are exported */ +#define PyGreenlet_API_pointers 12 + +#define PyGreenlet_Type_NUM 0 +#define PyExc_GreenletError_NUM 1 +#define PyExc_GreenletExit_NUM 2 + +#define PyGreenlet_New_NUM 3 +#define PyGreenlet_GetCurrent_NUM 4 +#define PyGreenlet_Throw_NUM 5 +#define PyGreenlet_Switch_NUM 6 +#define PyGreenlet_SetParent_NUM 7 + +#define PyGreenlet_MAIN_NUM 8 +#define PyGreenlet_STARTED_NUM 9 +#define PyGreenlet_ACTIVE_NUM 10 +#define PyGreenlet_GET_PARENT_NUM 11 + +#ifndef GREENLET_MODULE +/* This section is used by modules that uses the greenlet C API */ +static void** _PyGreenlet_API = NULL; + +# define PyGreenlet_Type \ + (*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM]) + +# define PyExc_GreenletError \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM]) + +# define PyExc_GreenletExit \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM]) + +/* + * PyGreenlet_New(PyObject *args) + * + * greenlet.greenlet(run, parent=None) + */ +# define PyGreenlet_New \ + (*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \ + _PyGreenlet_API[PyGreenlet_New_NUM]) + +/* + * PyGreenlet_GetCurrent(void) + * + * greenlet.getcurrent() + */ +# define PyGreenlet_GetCurrent \ + (*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM]) + +/* + * PyGreenlet_Throw( + * PyGreenlet *greenlet, + * PyObject *typ, + * PyObject *val, + * PyObject *tb) + * + * g.throw(...) + */ +# define PyGreenlet_Throw \ + (*(PyObject * (*)(PyGreenlet * self, \ + PyObject * typ, \ + PyObject * val, \ + PyObject * tb)) \ + _PyGreenlet_API[PyGreenlet_Throw_NUM]) + +/* + * PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args) + * + * g.switch(*args, **kwargs) + */ +# define PyGreenlet_Switch \ + (*(PyObject * \ + (*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \ + _PyGreenlet_API[PyGreenlet_Switch_NUM]) + +/* + * PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent) + * + * g.parent = new_parent + */ +# define PyGreenlet_SetParent \ + (*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \ + _PyGreenlet_API[PyGreenlet_SetParent_NUM]) + +/* + * PyGreenlet_GetParent(PyObject* greenlet) + * + * return greenlet.parent; + * + * This could return NULL even if there is no exception active. + * If it does not return NULL, you are responsible for decrementing the + * reference count. + */ +# define PyGreenlet_GetParent \ + (*(PyGreenlet* (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_GET_PARENT_NUM]) + +/* + * deprecated, undocumented alias. + */ +# define PyGreenlet_GET_PARENT PyGreenlet_GetParent + +# define PyGreenlet_MAIN \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_MAIN_NUM]) + +# define PyGreenlet_STARTED \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_STARTED_NUM]) + +# define PyGreenlet_ACTIVE \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_ACTIVE_NUM]) + + + + +/* Macro that imports greenlet and initializes C API */ +/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we + keep the older definition to be sure older code that might have a copy of + the header still works. */ +# define PyGreenlet_Import() \ + { \ + _PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \ + } + +#endif /* GREENLET_MODULE */ + +#ifdef __cplusplus +} +#endif +#endif /* !Py_GREENLETOBJECT_H */ diff --git a/asset_pilot_docker/.venv/lib64 b/asset_pilot_docker/.venv/lib64 new file mode 120000 index 0000000..7951405 --- /dev/null +++ b/asset_pilot_docker/.venv/lib64 @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/asset_pilot_docker/.venv/pyvenv.cfg b/asset_pilot_docker/.venv/pyvenv.cfg new file mode 100644 index 0000000..404dc80 --- /dev/null +++ b/asset_pilot_docker/.venv/pyvenv.cfg @@ -0,0 +1,5 @@ +home = /usr/bin +include-system-site-packages = false +version = 3.12.3 +executable = /usr/bin/python3.12 +command = /usr/bin/python3 -m venv /home/ubuntu/AssetPilot/asset_pilot_docker/.venv diff --git a/asset_pilot_docker/DOCKER_GUIDE.md b/asset_pilot_docker/DOCKER_GUIDE.md new file mode 100644 index 0000000..58bbe53 --- /dev/null +++ b/asset_pilot_docker/DOCKER_GUIDE.md @@ -0,0 +1,420 @@ +# Asset Pilot - Docker ์„ค์น˜ ๊ฐ€์ด๋“œ + +## ๐Ÿณ Docker ๋ฐฉ์‹์˜ ์žฅ์  + +- โœ… ๋…๋ฆฝ๋œ ์ปจํ…Œ์ด๋„ˆ๋กœ ๊น”๋”ํ•œ ํ™˜๊ฒฝ ๊ด€๋ฆฌ +- โœ… PostgreSQL๊ณผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ถ„๋ฆฌ +- โœ… ํ•œ ๋ฒˆ์˜ ๋ช…๋ น์œผ๋กœ ์ „์ฒด ์‹œ์Šคํ…œ ์‹คํ–‰ +- โœ… ์‰ฌ์šด ๋ฐฑ์—… ๋ฐ ๋ณต๊ตฌ +- โœ… ํฌํŠธ ์ถฉ๋Œ ์—†์Œ +- โœ… ์—…๋ฐ์ดํŠธ ๋ฐ ๋กค๋ฐฑ ๊ฐ„ํŽธ + +--- + +## ๐Ÿ“‹ ์‚ฌ์ „ ์ค€๋น„ + +### 1. Docker ์„ค์น˜ + +#### Orange Pi (Ubuntu/Debian) +```bash +# Docker ์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ +curl -fsSL https://get.docker.com -o get-docker.sh +sudo sh get-docker.sh + +# ํ˜„์žฌ ์‚ฌ์šฉ์ž๋ฅผ docker ๊ทธ๋ฃน์— ์ถ”๊ฐ€ +sudo usermod -aG docker $USER + +# ๋กœ๊ทธ์•„์›ƒ ํ›„ ์žฌ๋กœ๊ทธ์ธ ๋˜๋Š” +newgrp docker + +# Docker ์„œ๋น„์Šค ์‹œ์ž‘ +sudo systemctl start docker +sudo systemctl enable docker +``` + +#### Docker Compose ์„ค์น˜ (์ด๋ฏธ ํฌํ•จ๋˜์–ด ์žˆ์„ ์ˆ˜ ์žˆ์Œ) +```bash +# Docker Compose ๋ฒ„์ „ ํ™•์ธ +docker compose version + +# ์—†๋‹ค๋ฉด ์„ค์น˜ +sudo apt-get update +sudo apt-get install docker-compose-plugin +``` + +### 2. ์„ค์น˜ ํ™•์ธ +```bash +docker --version +docker compose version +``` + +--- + +## ๐Ÿš€ ์„ค์น˜ ๋ฐ ์‹คํ–‰ + +### 1๋‹จ๊ณ„: ํŒŒ์ผ ์—…๋กœ๋“œ + +Orange Pi์— `asset_pilot_docker.tar.gz` ํŒŒ์ผ์„ ์ „์†ก: + +```bash +# Windows์—์„œ (PowerShell) +scp asset_pilot_docker.tar.gz orangepi@192.168.1.100:~/ + +# Linux/Mac์—์„œ +scp asset_pilot_docker.tar.gz orangepi@192.168.1.100:~/ +``` + +### 2๋‹จ๊ณ„: ์••์ถ• ํ•ด์ œ + +```bash +# SSH ์ ‘์† +ssh orangepi@192.168.1.100 + +# ์••์ถ• ํ•ด์ œ +tar -xzf asset_pilot_docker.tar.gz +cd asset_pilot_docker +``` + +### 3๋‹จ๊ณ„: ํ™˜๊ฒฝ ์„ค์ • + +```bash +# .env ํŒŒ์ผ ํŽธ์ง‘ (๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ) +nano .env +``` + +`.env` ํŒŒ์ผ ๋‚ด์šฉ: +```env +DB_PASSWORD=your_secure_password_here # ์—ฌ๊ธฐ๋ฅผ ๋ณ€๊ฒฝํ•˜์„ธ์š”! +``` + +์ €์žฅ: `Ctrl + X` โ†’ `Y` โ†’ `Enter` + +### 4๋‹จ๊ณ„: Docker ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰ + +```bash +# ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์‹คํ–‰ +docker compose up -d + +# ์‹คํ–‰ ์ƒํƒœ ํ™•์ธ +docker compose ps +``` + +์ถœ๋ ฅ ์˜ˆ์‹œ: +``` +NAME IMAGE STATUS PORTS +asset_pilot_app asset_pilot_docker-app Up 30 seconds 0.0.0.0:8000->8000/tcp +asset_pilot_db postgres:16-alpine Up 30 seconds 0.0.0.0:5432->5432/tcp +``` + +### 5๋‹จ๊ณ„: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™” + +```bash +# ์•ฑ ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€์—์„œ ์ดˆ๊ธฐํ™” ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ +docker compose exec app python init_db.py +``` + +### 6๋‹จ๊ณ„: ์ ‘์† ํ™•์ธ + +์›น ๋ธŒ๋ผ์šฐ์ €์—์„œ: +``` +http://[Orange_Pi_IP]:8000 +``` + +์˜ˆ: `http://192.168.1.100:8000` + +--- + +## ๐Ÿ”ง Docker ๊ด€๋ฆฌ ๋ช…๋ น์–ด + +### ์ปจํ…Œ์ด๋„ˆ ๊ด€๋ฆฌ + +```bash +# ์ „์ฒด ์‹œ์ž‘ +docker compose up -d + +# ์ „์ฒด ์ค‘์ง€ +docker compose down + +# ์ „์ฒด ์žฌ์‹œ์ž‘ +docker compose restart + +# ํŠน์ • ์„œ๋น„์Šค๋งŒ ์žฌ์‹œ์ž‘ +docker compose restart app # ์•ฑ๋งŒ +docker compose restart postgres # DB๋งŒ + +# ์ƒํƒœ ํ™•์ธ +docker compose ps + +# ๋กœ๊ทธ ํ™•์ธ (์‹ค์‹œ๊ฐ„) +docker compose logs -f + +# ํŠน์ • ์„œ๋น„์Šค ๋กœ๊ทธ๋งŒ +docker compose logs -f app +docker compose logs -f postgres +``` + +### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ฆฌ + +```bash +# PostgreSQL ์ปจํ…Œ์ด๋„ˆ ์ ‘์† +docker compose exec postgres psql -U asset_user -d asset_pilot + +# SQL ์ฟผ๋ฆฌ ์‹คํ–‰ ์˜ˆ์‹œ +# \dt # ํ…Œ์ด๋ธ” ๋ชฉ๋ก +# \d assets # assets ํ…Œ์ด๋ธ” ๊ตฌ์กฐ +# SELECT * FROM assets; +# \q # ์ข…๋ฃŒ +``` + +### ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ด€๋ฆฌ + +```bash +# ์•ฑ ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€ ์ ‘์† +docker compose exec app /bin/bash + +# ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€์—์„œ Python ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ +docker compose exec app python init_db.py +``` + +--- + +## ๐Ÿ“Š ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ + +### ๋ฐฑ์—… + +#### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐฑ์—… +```bash +# ๋ฐฑ์—… ์ƒ์„ฑ +docker compose exec postgres pg_dump -U asset_user asset_pilot > backup_$(date +%Y%m%d).sql + +# ๋˜๋Š” +docker compose exec -T postgres pg_dump -U asset_user asset_pilot > backup.sql +``` + +#### ์ „์ฒด ๋ณผ๋ฅจ ๋ฐฑ์—… +```bash +# ๋ณผ๋ฅจ ๋ฐฑ์—… (๊ณ ๊ธ‰) +docker run --rm -v asset_pilot_docker_postgres_data:/data \ + -v $(pwd):/backup alpine tar czf /backup/postgres_backup.tar.gz /data +``` + +### ๋ณต์› + +```bash +# ๋ฐฑ์—… ํŒŒ์ผ ๋ณต์› +cat backup.sql | docker compose exec -T postgres psql -U asset_user -d asset_pilot +``` + +### CSV ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ (Windows ์•ฑ์—์„œ) + +```bash +# 1. CSV ํŒŒ์ผ์„ ์ปจํ…Œ์ด๋„ˆ๋กœ ๋ณต์‚ฌ +docker cp user_assets.csv asset_pilot_app:/app/ + +# 2. import_csv.py ์ƒ์„ฑ (์•„๋ž˜ ์Šคํฌ๋ฆฝํŠธ ์ฐธ๊ณ ) +docker compose exec app python import_csv.py user_assets.csv +``` + +--- + +## ๐Ÿ”„ ์—…๋ฐ์ดํŠธ + +### ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์—…๋ฐ์ดํŠธ + +```bash +# 1. ์ƒˆ ์ฝ”๋“œ ๋ฐ›๊ธฐ (ํŒŒ์ผ ์—…๋กœ๋“œ ๋˜๋Š” git pull) + +# 2. ์ด๋ฏธ์ง€ ์žฌ๋นŒ๋“œ +docker compose build app + +# 3. ์žฌ์‹œ์ž‘ +docker compose up -d app +``` + +### PostgreSQL ์—…๋ฐ์ดํŠธ + +```bash +# ์ฃผ์˜: ๋ฐ์ดํ„ฐ ๋ฐฑ์—… ํ•„์ˆ˜! +# 1. ๋ฐฑ์—… ์ƒ์„ฑ +docker compose exec -T postgres pg_dump -U asset_user asset_pilot > backup.sql + +# 2. docker-compose.yml์—์„œ ๋ฒ„์ „ ๋ณ€๊ฒฝ (์˜ˆ: postgres:17-alpine) + +# 3. ์ปจํ…Œ์ด๋„ˆ ์žฌ์ƒ์„ฑ +docker compose down +docker compose up -d +``` + +--- + +## ๐Ÿ—‘๏ธ ์™„์ „ ์‚ญ์ œ + +```bash +# ์ปจํ…Œ์ด๋„ˆ ์ค‘์ง€ ๋ฐ ์‚ญ์ œ +docker compose down + +# ๋ณผ๋ฅจ๊นŒ์ง€ ์‚ญ์ œ (๋ฐ์ดํ„ฐ ์™„์ „ ์‚ญ์ œ!) +docker compose down -v + +# ์ด๋ฏธ์ง€๋„ ์‚ญ์ œ +docker rmi asset_pilot_docker-app postgres:16-alpine +``` + +--- + +## ๐Ÿ› ๏ธ ๋ฌธ์ œ ํ•ด๊ฒฐ + +### ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์‹œ์ž‘๋˜์ง€ ์•Š์Œ + +```bash +# ๋กœ๊ทธ ํ™•์ธ +docker compose logs + +# ํŠน์ • ์„œ๋น„์Šค ๋กœ๊ทธ +docker compose logs app +docker compose logs postgres + +# ์ปจํ…Œ์ด๋„ˆ ์ƒํƒœ ํ™•์ธ +docker compose ps -a +``` + +### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์˜ค๋ฅ˜ + +```bash +# PostgreSQL ์ปจํ…Œ์ด๋„ˆ ํ—ฌ์Šค์ฒดํฌ +docker compose exec postgres pg_isready -U asset_user -d asset_pilot + +# ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ +docker compose exec postgres psql -U asset_user -d asset_pilot -c "SELECT 1;" +``` + +### ํฌํŠธ ์ถฉ๋Œ + +```bash +# 8000๋ฒˆ ํฌํŠธ ์‚ฌ์šฉ ํ™•์ธ +sudo lsof -i :8000 + +# docker-compose.yml์—์„œ ํฌํŠธ ๋ณ€๊ฒฝ (์˜ˆ: 8001:8000) +``` + +### ๋””์Šคํฌ ๊ณต๊ฐ„ ๋ถ€์กฑ + +```bash +# ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” Docker ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ +docker system prune -a + +# ๋ณผ๋ฅจ ํ™•์ธ +docker volume ls +``` + +--- + +## ๐Ÿ“ฑ ์›๊ฒฉ ์ ‘๊ทผ ์„ค์ • + +### Nginx ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ (์„ ํƒ์ ) + +```bash +# Nginx ์„ค์น˜ +sudo apt install nginx + +# ์„ค์ • ํŒŒ์ผ ์ƒ์„ฑ +sudo nano /etc/nginx/sites-available/asset_pilot +``` + +์„ค์ • ๋‚ด์šฉ: +```nginx +server { + listen 80; + server_name your_domain.com; # ๋˜๋Š” IP ์ฃผ์†Œ + + location / { + proxy_pass http://localhost:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /api/stream { + proxy_pass http://localhost:8000/api/stream; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_buffering off; + proxy_cache off; + } +} +``` + +ํ™œ์„ฑํ™”: +```bash +sudo ln -s /etc/nginx/sites-available/asset_pilot /etc/nginx/sites-enabled/ +sudo nginx -t +sudo systemctl restart nginx +``` + +--- + +## ๐Ÿ” ๋ณด์•ˆ ๊ถŒ์žฅ์‚ฌํ•ญ + +### 1. .env ํŒŒ์ผ ๋ณดํ˜ธ +```bash +chmod 600 .env +``` + +### 2. ๋ฐฉํ™”๋ฒฝ ์„ค์ • +```bash +# 8000๋ฒˆ ํฌํŠธ๋งŒ ํ—ˆ์šฉ (์™ธ๋ถ€ ์ ‘๊ทผ ์‹œ) +sudo ufw allow 8000/tcp + +# PostgreSQL ํฌํŠธ๋Š” ์™ธ๋ถ€ ์ฐจ๋‹จ (๊ธฐ๋ณธ๊ฐ’) +sudo ufw deny 5432/tcp +``` + +### 3. ์ •๊ธฐ ๋ฐฑ์—… +```bash +# cron์œผ๋กœ ๋งค์ผ ์ž๋™ ๋ฐฑ์—… +crontab -e + +# ์ถ”๊ฐ€ (๋งค์ผ ์ƒˆ๋ฒฝ 3์‹œ) +0 3 * * * cd /home/orangepi/asset_pilot_docker && docker compose exec -T postgres pg_dump -U asset_user asset_pilot > backup_$(date +\%Y\%m\%d).sql +``` + +--- + +## ๐Ÿ“Š ์‹œ์Šคํ…œ ๋ฆฌ์†Œ์Šค ๋ชจ๋‹ˆํ„ฐ๋ง + +```bash +# ์ปจํ…Œ์ด๋„ˆ ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ๋Ÿ‰ +docker stats + +# ํŠน์ • ์ปจํ…Œ์ด๋„ˆ๋งŒ +docker stats asset_pilot_app asset_pilot_db +``` + +--- + +## โœ… ์„ค์น˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +- [ ] Docker ์„ค์น˜ ์™„๋ฃŒ +- [ ] Docker Compose ์„ค์น˜ ์™„๋ฃŒ +- [ ] ํ”„๋กœ์ ํŠธ ํŒŒ์ผ ์••์ถ• ํ•ด์ œ +- [ ] .env ํŒŒ์ผ ๋น„๋ฐ€๋ฒˆํ˜ธ ์„ค์ • +- [ ] `docker compose up -d` ์‹คํ–‰ +- [ ] ์ปจํ…Œ์ด๋„ˆ ์ƒํƒœ ํ™•์ธ (`docker compose ps`) +- [ ] ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™” (`docker compose exec app python init_db.py`) +- [ ] ์›น ๋ธŒ๋ผ์šฐ์ € ์ ‘์† ํ™•์ธ (`http://[IP]:8000`) +- [ ] ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ๋™์ž‘ ํ™•์ธ + +--- + +## ๐ŸŽ‰ ์™„๋ฃŒ! + +๋ชจ๋“  ๊ณผ์ •์ด ์™„๋ฃŒ๋˜๋ฉด ๋‹ค์Œ URL๋กœ ์ ‘์†ํ•˜์„ธ์š”: +``` +http://[Orange_Pi_IP]:8000 +``` + +๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๋กœ๊ทธ๋ฅผ ํ™•์ธํ•˜์„ธ์š”: +```bash +docker compose logs -f +``` diff --git a/asset_pilot_docker/Dockerfile b/asset_pilot_docker/Dockerfile new file mode 100644 index 0000000..2dc650f --- /dev/null +++ b/asset_pilot_docker/Dockerfile @@ -0,0 +1,34 @@ +FROM python:3.11-slim + +# ์ž‘์—… ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ • +WORKDIR /app + +# ์‹œ์Šคํ…œ ํŒจํ‚ค์ง€ ์—…๋ฐ์ดํŠธ ๋ฐ ํ•„์ˆ˜ ํŒจํ‚ค์ง€ ์„ค์น˜ +RUN apt-get update && apt-get install -y \ + gcc \ + postgresql-client \ + libpq-dev \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Python ์˜์กด์„ฑ ํŒŒ์ผ ๋ณต์‚ฌ ๋ฐ ์„ค์น˜ +COPY requirements.txt . +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + +# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํŒŒ์ผ ๋ณต์‚ฌ (๊ตฌ์กฐ๋ฅผ ์œ ์ง€ํ•˜๋ฉฐ ๋ณต์‚ฌ) +COPY . . + +# ๋กœ๊ทธ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ +RUN mkdir -p /app/logs + +# ๋น„๋ฃจํŠธ ์‚ฌ์šฉ์ž ์ƒ์„ฑ ๋ฐ ๊ถŒํ•œ ์„ค์ • +RUN useradd -m -u 1000 appuser && \ + chown -R appuser:appuser /app +USER appuser + +# ํ—ฌ์Šค์ฒดํฌ ์—”๋“œํฌ์ธํŠธ ๋…ธ์ถœ +EXPOSE 8000 + +# ์ปจํ…Œ์ด๋„ˆ ์‹œ์ž‘ ์‹œ ์‹คํ–‰ํ•  ๋ช…๋ น์–ด (uvicorn์œผ๋กœ ์ง์ ‘ ์‹คํ–‰ ๊ถŒ์žฅ) +CMD ["python", "main.py"] \ No newline at end of file diff --git a/asset_pilot_docker/README.md b/asset_pilot_docker/README.md new file mode 100644 index 0000000..8536a81 --- /dev/null +++ b/asset_pilot_docker/README.md @@ -0,0 +1,159 @@ +# Asset Pilot - Docker Edition + +๐Ÿณ Docker ์ปจํ…Œ์ด๋„ˆ ๊ธฐ๋ฐ˜ ์ž์‚ฐ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ + +## ๐Ÿ“ฆ ๊ตฌ์„ฑ + +์ด ํ”„๋กœ์ ํŠธ๋Š” 2๊ฐœ์˜ ๋…๋ฆฝ๋œ Docker ์ปจํ…Œ์ด๋„ˆ๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค: + +1. **PostgreSQL ์ปจํ…Œ์ด๋„ˆ** (`asset_pilot_db`) + - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„œ๋ฒ„ + - ํฌํŠธ: 5432 + - ๋ณผ๋ฅจ: `postgres_data` + +2. **Asset Pilot ์•ฑ ์ปจํ…Œ์ด๋„ˆ** (`asset_pilot_app`) + - FastAPI ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ + - ํฌํŠธ: 8000 + - ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ๋ฐ ์ œ๊ณต + +## ๐Ÿš€ ๋น ๋ฅธ ์‹œ์ž‘ + +### ์ž๋™ ์„ค์น˜ (๊ถŒ์žฅ) + +```bash +# ์••์ถ• ํ•ด์ œ +tar -xzf asset_pilot_docker.tar.gz +cd asset_pilot_docker + +# ์ž๋™ ์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ +bash start.sh +``` + +### ์ˆ˜๋™ ์„ค์น˜ + +```bash +# 1. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • +nano .env +# DB_PASSWORD๋ฅผ ์›ํ•˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ๋ณ€๊ฒฝ + +# 2. Docker ์ปจํ…Œ์ด๋„ˆ ์‹œ์ž‘ +docker compose up -d + +# 3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™” +docker compose exec app python init_db.py + +# 4. ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ ‘์† +# http://[IP์ฃผ์†Œ]:8000 +``` + +## ๐Ÿ“ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ + +``` +asset_pilot_docker/ +โ”œโ”€โ”€ app/ # ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ”๋“œ +โ”‚ โ”œโ”€โ”€ calculator.py +โ”‚ โ”œโ”€โ”€ database.py +โ”‚ โ”œโ”€โ”€ fetcher.py +โ”‚ โ””โ”€โ”€ models.py +โ”œโ”€โ”€ static/ # ์ •์  ํŒŒ์ผ +โ”‚ โ”œโ”€โ”€ css/ +โ”‚ โ””โ”€โ”€ js/ +โ”œโ”€โ”€ templates/ # HTML ํ…œํ”Œ๋ฆฟ +โ”‚ โ””โ”€โ”€ index.html +โ”œโ”€โ”€ docker-compose.yml # Docker Compose ์„ค์ • +โ”œโ”€โ”€ Dockerfile # ์•ฑ ์ปจํ…Œ์ด๋„ˆ ์ด๋ฏธ์ง€ +โ”œโ”€โ”€ .env # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ +โ”œโ”€โ”€ main.py # FastAPI ๋ฉ”์ธ +โ”œโ”€โ”€ init_db.py # DB ์ดˆ๊ธฐํ™” +โ”œโ”€โ”€ import_csv.py # CSV ๊ฐ€์ ธ์˜ค๊ธฐ +โ”œโ”€โ”€ start.sh # ์ž๋™ ์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ +โ””โ”€โ”€ DOCKER_GUIDE.md # ์ƒ์„ธ ๊ฐ€์ด๋“œ +``` + +## ๐Ÿ”ง ์ฃผ์š” ๋ช…๋ น์–ด + +### ์ปจํ…Œ์ด๋„ˆ ๊ด€๋ฆฌ +```bash +# ์‹œ์ž‘ +docker compose up -d + +# ์ค‘์ง€ +docker compose down + +# ์žฌ์‹œ์ž‘ +docker compose restart + +# ์ƒํƒœ ํ™•์ธ +docker compose ps + +# ๋กœ๊ทธ ๋ณด๊ธฐ +docker compose logs -f +``` + +### ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ +```bash +# DB ๋ฐฑ์—… +docker compose exec postgres pg_dump -U asset_user asset_pilot > backup.sql + +# DB ๋ณต์› +cat backup.sql | docker compose exec -T postgres psql -U asset_user -d asset_pilot + +# CSV ๊ฐ€์ ธ์˜ค๊ธฐ +docker cp user_assets.csv asset_pilot_app:/app/ +docker compose exec app python import_csv.py user_assets.csv +``` + +## ๐ŸŒ ์ ‘์† + +``` +http://localhost:8000 # ๋กœ์ปฌ +http://[IP์ฃผ์†Œ]:8000 # ๋„คํŠธ์›Œํฌ +``` + +## ๐Ÿ“š ๋ฌธ์„œ + +- **DOCKER_GUIDE.md** - Docker ์ƒ์„ธ ์„ค์น˜ ๋ฐ ๊ด€๋ฆฌ ๊ฐ€์ด๋“œ +- ๋ฌธ์ œ ํ•ด๊ฒฐ, ๋ฐฑ์—…, ์—…๋ฐ์ดํŠธ ๋ฐฉ๋ฒ• ํฌํ•จ + +## ๐Ÿ” ๋ณด์•ˆ + +- `.env` ํŒŒ์ผ์— ๋น„๋ฐ€๋ฒˆํ˜ธ ์ €์žฅ (๊ถŒํ•œ: 600) +- PostgreSQL์€ ๋‚ด๋ถ€ ๋„คํŠธ์›Œํฌ๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ +- ๋ฐฉํ™”๋ฒฝ์—์„œ ํ•„์š”ํ•œ ํฌํŠธ๋งŒ ๊ฐœ๋ฐฉ + +## ๐Ÿ†˜ ๋ฌธ์ œ ํ•ด๊ฒฐ + +### ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์‹œ์ž‘๋˜์ง€ ์•Š์Œ +```bash +docker compose logs +``` + +### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์˜ค๋ฅ˜ +```bash +docker compose exec postgres pg_isready -U asset_user -d asset_pilot +``` + +### ํฌํŠธ ์ถฉ๋Œ +```bash +# docker-compose.yml์—์„œ ํฌํŠธ ๋ณ€๊ฒฝ +# "8001:8000" ์œผ๋กœ ์ˆ˜์ • +``` + +## ๐Ÿ“Š ์‹œ์Šคํ…œ ์š”๊ตฌ์‚ฌํ•ญ + +- Docker 20.10+ +- Docker Compose 2.0+ +- ์ตœ์†Œ 2GB RAM +- ์ตœ์†Œ 5GB ๋””์Šคํฌ ๊ณต๊ฐ„ + +## ๐ŸŽฏ ํŠน์ง• + +โœ… ๋…๋ฆฝ๋œ ์ปจํ…Œ์ด๋„ˆ๋กœ ์‹œ์Šคํ…œ ๊ฒฉ๋ฆฌ +โœ… ํ•œ ๋ฒˆ์˜ ๋ช…๋ น์œผ๋กœ ์ „์ฒด ์‹œ์Šคํ…œ ์‹คํ–‰ +โœ… ์‰ฌ์šด ๋ฐฑ์—… ๋ฐ ๋ณต๊ตฌ +โœ… ์—…๋ฐ์ดํŠธ ๋ฐ ๋กค๋ฐฑ ๊ฐ„ํŽธ +โœ… ๊ฐœ๋ฐœ/ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ ์ผ๊ด€์„ฑ + +--- + +**Asset Pilot Docker Edition v1.0** diff --git a/asset_pilot_docker/app/calculator.py b/asset_pilot_docker/app/calculator.py new file mode 100644 index 0000000..13220e6 --- /dev/null +++ b/asset_pilot_docker/app/calculator.py @@ -0,0 +1,59 @@ +from typing import Dict, Optional + +class Calculator: + """์†์ต ๊ณ„์‚ฐ ํด๋ž˜์Šค""" + + @staticmethod + def calc_pnl( + gold_buy_price: float, + gold_quantity: float, + btc_buy_price: float, + btc_quantity: float, + current_gold: Optional[float], + current_btc: Optional[float] + ) -> Dict: + """ + ๊ธˆ๊ณผ BTC์˜ ์†์ต ๊ณ„์‚ฐ + + Returns: + { + "๊ธˆ์†์ต": float, + "๊ธˆ์†์ต%": float, + "BTC์†์ต": float, + "BTC์†์ต%": float, + "์ด์†์ต": float, + "์ด์†์ต%": float + } + """ + result = { + "๊ธˆ์†์ต": 0.0, + "๊ธˆ์†์ต%": 0.0, + "BTC์†์ต": 0.0, + "BTC์†์ต%": 0.0, + "์ด์†์ต": 0.0, + "์ด์†์ต%": 0.0 + } + + # ๊ธˆ ์†์ต ๊ณ„์‚ฐ + if current_gold: + cost_gold = gold_buy_price * gold_quantity + pnl_gold = gold_quantity * (float(current_gold) - gold_buy_price) + result["๊ธˆ์†์ต"] = round(pnl_gold, 0) + if cost_gold > 0: + result["๊ธˆ์†์ต%"] = round((pnl_gold / cost_gold * 100), 2) + + # BTC ์†์ต ๊ณ„์‚ฐ + if current_btc: + cost_btc = btc_buy_price * btc_quantity + pnl_btc = btc_quantity * (float(current_btc) - btc_buy_price) + result["BTC์†์ต"] = round(pnl_btc, 0) + if cost_btc > 0: + result["BTC์†์ต%"] = round((pnl_btc / cost_btc * 100), 2) + + # ์ด ์†์ต ๊ณ„์‚ฐ + result["์ด์†์ต"] = result["๊ธˆ์†์ต"] + result["BTC์†์ต"] + total_cost = (gold_buy_price * gold_quantity) + (btc_buy_price * btc_quantity) + if total_cost > 0: + result["์ด์†์ต%"] = round((result["์ด์†์ต"] / total_cost * 100), 2) + + return result diff --git a/asset_pilot_docker/app/database.py b/asset_pilot_docker/app/database.py new file mode 100644 index 0000000..85c9eaf --- /dev/null +++ b/asset_pilot_docker/app/database.py @@ -0,0 +1,29 @@ +import os +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from dotenv import load_dotenv + +load_dotenv() + +# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค URL ๊ฐ€์ ธ์˜ค๊ธฐ +DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://asset_user:password@localhost/asset_pilot") + +# SQLAlchemy ์—”์ง„ ์ƒ์„ฑ +engine = create_engine( + DATABASE_URL, + pool_size=10, + max_overflow=20, + pool_pre_ping=True, # ์—ฐ๊ฒฐ ์œ ํšจ์„ฑ ์ž๋™ ํ™•์ธ + echo=False # SQL ์ฟผ๋ฆฌ ๋กœ๊ทธ (๋””๋ฒ„๊น… ์‹œ True๋กœ ๋ณ€๊ฒฝ) +) + +# ์„ธ์…˜ ํŒฉํ† ๋ฆฌ ์ƒ์„ฑ +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +def get_db(): + """๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ธ์…˜ ์˜์กด์„ฑ""" + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/asset_pilot_docker/app/fetcher.py b/asset_pilot_docker/app/fetcher.py new file mode 100644 index 0000000..33627a0 --- /dev/null +++ b/asset_pilot_docker/app/fetcher.py @@ -0,0 +1,113 @@ +import requests +import re +from typing import Dict, Optional +import time + +class DataFetcher: + def __init__(self): + self.session = requests.Session() + self.session.headers.update({ + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36' + }) + + def fetch_investing_com(self, asset_code: str) -> Optional[float]: + """์ธ๋ฒ ์ŠคํŒ…๋‹ท์ปด (์œˆ๋„์šฐ ์•ฑ ๋ฐฉ์‹ ์ •๊ทœ์‹ ์ ์šฉ)""" + try: + url = f"https://www.investing.com/currencies/{asset_code.lower().replace('/', '-')}" + if asset_code == "USD/DXY": + url = "https://www.investing.com/indices/usdollar" + + # allow_redirects๋ฅผ True๋กœ ํ•˜์—ฌ ์ฃผ์†Œ ๋ณ€๊ฒฝ์— ๋Œ€์‘ + response = self.session.get(url, timeout=10, allow_redirects=True) + html = response.text + + # ์œˆ๋„์šฐ์—์„œ ๊ฐ€์žฅ ์ž˜ ๋˜๋˜ ํŒจํ„ด ์ˆœ์„œ๋Œ€๋กœ ์‹œ๋„ + patterns = [ + r'data-test="instrument-price-last">([\d,.]+)<', + r'last_last">([\d,.]+)<', + r'instrument-price-last">([\d,.]+)<' + ] + for pattern in patterns: + p = re.search(pattern, html) + if p: + return float(p.group(1).replace(',', '')) + except Exception as e: + print(f"โš ๏ธ Investing ์ˆ˜์ง‘ ์‹คํŒจ ({asset_code}): {e}") + return None + + def fetch_binance(self) -> Optional[float]: + """๋ฐ”์ด๋‚ธ์Šค BTC/USDT (๋ณด๋‚ด์ฃผ์‹  ์œˆ๋„์šฐ ์ฝ”๋“œ ๋กœ์ง)""" + url = "https://api.binance.com/api/v3/ticker/price" + try: + response = requests.get(url, params={"symbol": "BTCUSDT"}, timeout=5) + response.raise_for_status() + return float(response.json()["price"]) + except Exception as e: + print(f"โŒ Binance API ์‹คํŒจ: {e}") + return None + + def fetch_upbit(self) -> Optional[float]: + """์—…๋น„ํŠธ BTC/KRW (๋ณด๋‚ด์ฃผ์‹  ์œˆ๋„์šฐ ์ฝ”๋“œ ๋กœ์ง)""" + url = "https://api.upbit.com/v1/ticker" + try: + response = requests.get(url, params={"markets": "KRW-BTC"}, timeout=5) + response.raise_for_status() + data = response.json() + return float(data[0]["trade_price"]) if data else None + except Exception as e: + print(f"โŒ Upbit API ์‹คํŒจ: {e}") + return None + + def fetch_usd_krw(self) -> Optional[float]: + """USD/KRW ํ™˜์œจ (DNS ์—๋Ÿฌ ๋ฐฉ์ง€ ์ด์ค‘ํ™”)""" + # ๋ฐฉ๋ฒ• 1: ๋‘๋‚˜๋ฌด CDN (์›๋ž˜ ์ฃผ์†Œ) + try: + url = "https://quotation-api-cdn.dunamu.com/v1/forex/recent?codes=FRX.KRWUSD" + res = requests.get(url, timeout=3) + if res.status_code == 200: + return float(res.json()[0]["basePrice"]) + except: + pass # ์‹คํŒจํ•˜๋ฉด ๋ฐ”๋กœ ์ธ๋ฒ ์ŠคํŒ…๋‹ท์ปด์œผ๋กœ ์ „ํ™˜ + + # ๋ฐฉ๋ฒ• 2: ์ธ๋ฒ ์ŠคํŒ…๋‹ท์ปด์—์„œ ํ™˜์œจ ๊ฐ€์ ธ์˜ค๊ธฐ (๊ฐ€์žฅ ํ™•์‹คํ•œ ๋ฐฑ์—…) + return self.fetch_investing_com("USD/KRW") + + def fetch_krx_gold(self) -> Optional[float]: + """๊ธˆ ์‹œ์„ธ (๋„ค์ด๋ฒ„ ๊ธˆ์œต ๋ชจ๋ฐ”์ผ)""" + try: + url = "https://m.stock.naver.com/marketindex/metals/M04020000" + res = requests.get(url, timeout=5) + m = re.search(r'\"closePrice\":\"([\d,]+)\"', res.text) + return float(m.group(1).replace(",", "")) if m else None + except: + return None + + def fetch_all(self) -> Dict[str, Dict]: + print(f"๐Ÿ“Š [{time.strftime('%H:%M:%S')}] ์ˆ˜์ง‘ ์‹œ์ž‘...") + + # 1. ํ™˜์œจ ๋จผ์ € ์ˆ˜์ง‘ (๊ณ„์‚ฐ์˜ ํ•ต์‹ฌ) + usd_krw = self.fetch_usd_krw() + + # 2. ๋‚˜๋จธ์ง€ ์ž์‚ฐ ์ˆ˜์ง‘ + results = { + "XAU/USD": {"๊ฐ€๊ฒฉ": self.fetch_investing_com("XAU/USD"), "๋‹จ์œ„": "USD/oz"}, + "XAU/CNY": {"๊ฐ€๊ฒฉ": self.fetch_investing_com("XAU/CNY"), "๋‹จ์œ„": "CNY/oz"}, + "XAU/GBP": {"๊ฐ€๊ฒฉ": self.fetch_investing_com("XAU/GBP"), "๋‹จ์œ„": "GBP/oz"}, + "USD/DXY": {"๊ฐ€๊ฒฉ": self.fetch_investing_com("USD/DXY"), "๋‹จ์œ„": "Index"}, + "USD/KRW": {"๊ฐ€๊ฒฉ": usd_krw, "๋‹จ์œ„": "KRW"}, + "BTC/USD": {"๊ฐ€๊ฒฉ": self.fetch_binance(), "๋‹จ์œ„": "USDT"}, + "BTC/KRW": {"๊ฐ€๊ฒฉ": self.fetch_upbit(), "๋‹จ์œ„": "KRW"}, + "KRX/GLD": {"๊ฐ€๊ฒฉ": self.fetch_krx_gold(), "๋‹จ์œ„": "KRW/g"}, + } + + # 3. XAU/KRW ๊ณ„์‚ฐ + xau_krw = None + if results["XAU/USD"]["๊ฐ€๊ฒฉ"] and usd_krw: + xau_krw = round((results["XAU/USD"]["๊ฐ€๊ฒฉ"] / 31.1034768) * usd_krw, 0) + results["XAU/KRW"] = {"๊ฐ€๊ฒฉ": xau_krw, "๋‹จ์œ„": "KRW/g"} + + success_count = sum(1 for v in results.values() if v['๊ฐ€๊ฒฉ'] is not None) + print(f"โœ… ์ˆ˜์ง‘ ์™„๋ฃŒ (์„ฑ๊ณต: {success_count}/9)") + return results + +fetcher = DataFetcher() \ No newline at end of file diff --git a/asset_pilot_docker/app/fetcher.py.claude b/asset_pilot_docker/app/fetcher.py.claude new file mode 100644 index 0000000..9f014d8 --- /dev/null +++ b/asset_pilot_docker/app/fetcher.py.claude @@ -0,0 +1,142 @@ +import requests +from typing import Dict, Optional +from bs4 import BeautifulSoup +import time + +class DataFetcher: + """๋ชจ๋“  ์ž์‚ฐ ๊ฐ€๊ฒฉ ์ˆ˜์ง‘ ํด๋ž˜์Šค""" + + def __init__(self): + self.session = requests.Session() + self.session.headers.update({ + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + }) + self.investing_cache = {} + self.cache_time = 0 + + def fetch_investing_com(self, asset_code: str) -> Optional[float]: + """Investing.com์—์„œ ๊ฐ€๊ฒฉ ์ˆ˜์ง‘""" + # ๊ฐ„๋‹จํ•œ ์บ์‹ฑ (5์ดˆ) + if time.time() - self.cache_time < 5 and asset_code in self.investing_cache: + return self.investing_cache[asset_code] + + asset_map = { + "XAU/USD": "8830", + "XAU/CNY": "2186", + "XAU/GBP": "8500", + "USD/DXY": "8827" + } + + asset_id = asset_map.get(asset_code) + if not asset_id: + return None + + try: + url = f"https://www.investing.com/currencies/{asset_code.lower().replace('/', '-')}" + response = self.session.get(url, timeout=5) + response.raise_for_status() + + soup = BeautifulSoup(response.text, 'lxml') + price_elem = soup.select_one('[data-test="instrument-price-last"]') + + if price_elem: + price_text = price_elem.text.strip().replace(',', '') + price = float(price_text) + self.investing_cache[asset_code] = price + return price + except Exception as e: + print(f"Investing.com ์ˆ˜์ง‘ ์‹คํŒจ ({asset_code}): {e}") + + return None + + def fetch_binance(self) -> Optional[float]: + """๋ฐ”์ด๋‚ธ์Šค BTC/USDT ๊ฐ€๊ฒฉ""" + try: + url = "https://api.binance.com/api/v3/ticker/price" + response = self.session.get(url, params={"symbol": "BTCUSDT"}, timeout=5) + response.raise_for_status() + data = response.json() + return float(data["price"]) if "price" in data else None + except Exception as e: + print(f"Binance API ์‹คํŒจ: {e}") + return None + + def fetch_upbit(self) -> Optional[float]: + """์—…๋น„ํŠธ BTC/KRW ๊ฐ€๊ฒฉ""" + try: + url = "https://api.upbit.com/v1/ticker" + response = self.session.get(url, params={"markets": "KRW-BTC"}, timeout=5) + response.raise_for_status() + data = response.json() + return data[0]["trade_price"] if data and "trade_price" in data[0] else None + except Exception as e: + print(f"Upbit API ์‹คํŒจ: {e}") + return None + + def fetch_usd_krw(self) -> Optional[float]: + """USD/KRW ํ™˜์œจ""" + try: + url = "https://quotation-api-cdn.dunamu.com/v1/forex/recent?codes=FRX.KRWUSD" + response = self.session.get(url, timeout=5) + response.raise_for_status() + data = response.json() + return data[0]["basePrice"] if data else None + except Exception as e: + print(f"USD/KRW ์ˆ˜์ง‘ ์‹คํŒจ: {e}") + return None + + def fetch_krx_gold(self) -> Optional[float]: + """ํ•œ๊ตญ๊ฑฐ๋ž˜์†Œ ๊ธˆ ํ˜„๋ฌผ ๊ฐ€๊ฒฉ""" + try: + url = "http://www.goldpr.co.kr/gms/default.asp" + response = self.session.get(url, timeout=5) + response.encoding = 'euc-kr' + + soup = BeautifulSoup(response.text, 'lxml') + + # ๊ธˆ ํ˜„๋ฌผ ๊ฐ€๊ฒฉ ํŒŒ์‹ฑ (์‚ฌ์ดํŠธ ๊ตฌ์กฐ์— ๋”ฐ๋ผ ์กฐ์ • ํ•„์š”) + price_elem = soup.select_one('table tr:nth-of-type(2) td:nth-of-type(2)') + if price_elem: + price_text = price_elem.text.strip().replace(',', '').replace('์›', '') + return float(price_text) + except Exception as e: + print(f"KRX ๊ธˆ ๊ฐ€๊ฒฉ ์ˆ˜์ง‘ ์‹คํŒจ: {e}") + + return None + + def fetch_all(self) -> Dict[str, Dict]: + """๋ชจ๋“  ์ž์‚ฐ ๊ฐ€๊ฒฉ ์ˆ˜์ง‘""" + print("๐Ÿ“Š ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์‹œ์ž‘...") + + # ๊ฐœ๋ณ„ ์ž์‚ฐ ์ˆ˜์ง‘ + xau_usd = self.fetch_investing_com("XAU/USD") + xau_cny = self.fetch_investing_com("XAU/CNY") + xau_gbp = self.fetch_investing_com("XAU/GBP") + usd_dxy = self.fetch_investing_com("USD/DXY") + usd_krw = self.fetch_usd_krw() + btc_usd = self.fetch_binance() + btc_krw = self.fetch_upbit() + krx_gold = self.fetch_krx_gold() + + # XAU/KRW ๊ณ„์‚ฐ (ํŠธ๋กœ์ด์˜จ์Šค -> ๊ทธ๋žจ๋‹น ์›ํ™”) + xau_krw = None + if xau_usd and usd_krw: + xau_krw = round((xau_usd / 31.1034768) * usd_krw, 0) + + results = { + "XAU/USD": {"๊ฐ€๊ฒฉ": xau_usd, "๋‹จ์œ„": "USD/oz"}, + "XAU/CNY": {"๊ฐ€๊ฒฉ": xau_cny, "๋‹จ์œ„": "CNY/oz"}, + "XAU/GBP": {"๊ฐ€๊ฒฉ": xau_gbp, "๋‹จ์œ„": "GBP/oz"}, + "USD/DXY": {"๊ฐ€๊ฒฉ": usd_dxy, "๋‹จ์œ„": "Index"}, + "USD/KRW": {"๊ฐ€๊ฒฉ": usd_krw, "๋‹จ์œ„": "KRW"}, + "BTC/USD": {"๊ฐ€๊ฒฉ": btc_usd, "๋‹จ์œ„": "USDT"}, + "BTC/KRW": {"๊ฐ€๊ฒฉ": btc_krw, "๋‹จ์œ„": "KRW"}, + "KRX/GLD": {"๊ฐ€๊ฒฉ": krx_gold, "๋‹จ์œ„": "KRW/g"}, + "XAU/KRW": {"๊ฐ€๊ฒฉ": xau_krw, "๋‹จ์œ„": "KRW/g"}, + } + + print(f"โœ… ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์™„๋ฃŒ (์„ฑ๊ณต: {sum(1 for v in results.values() if v['๊ฐ€๊ฒฉ'])}/9)") + return results + +# ์ „์—ญ ์ธ์Šคํ„ด์Šค +fetcher = DataFetcher() diff --git a/asset_pilot_docker/app/models.py b/asset_pilot_docker/app/models.py new file mode 100644 index 0000000..9e03593 --- /dev/null +++ b/asset_pilot_docker/app/models.py @@ -0,0 +1,55 @@ +from sqlalchemy import Column, Integer, String, Float, DateTime, Text, ForeignKey +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relationship +from datetime import datetime + +Base = declarative_base() + +class Asset(Base): + """์ž์‚ฐ ๋งˆ์Šคํ„ฐ ํ…Œ์ด๋ธ”""" + __tablename__ = "assets" + + id = Column(Integer, primary_key=True, index=True) + symbol = Column(String(20), unique=True, nullable=False, index=True) + name = Column(String(100), nullable=False) + category = Column(String(50)) # ๊ท€๊ธˆ์†, ์•”ํ˜ธํ™”ํ, ํ™˜์œจ ๋“ฑ + created_at = Column(DateTime, default=datetime.utcnow) + + # ๊ด€๊ณ„ + user_assets = relationship("UserAsset", back_populates="asset") + price_history = relationship("PriceHistory", back_populates="asset") + +class UserAsset(Base): + """์‚ฌ์šฉ์ž ์ž์‚ฐ ์ •๋ณด""" + __tablename__ = "user_assets" + + id = Column(Integer, primary_key=True, index=True) + asset_id = Column(Integer, ForeignKey("assets.id"), nullable=False) + previous_close = Column(Float, default=0.0) # ์ „์ผ์ข…๊ฐ€ + average_price = Column(Float, default=0.0) # ํ‰๊ท ๋งค์ž…๊ฐ€ + quantity = Column(Float, default=0.0) # ๋ณด์œ ๋Ÿ‰ + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # ๊ด€๊ณ„ + asset = relationship("Asset", back_populates="user_assets") + +class PriceHistory(Base): + """๊ฐ€๊ฒฉ ํžˆ์Šคํ† ๋ฆฌ (์„ ํƒ์ )""" + __tablename__ = "price_history" + + id = Column(Integer, primary_key=True, index=True) + asset_id = Column(Integer, ForeignKey("assets.id"), nullable=False) + price = Column(Float, nullable=False) + timestamp = Column(DateTime, default=datetime.utcnow, index=True) + + # ๊ด€๊ณ„ + asset = relationship("Asset", back_populates="price_history") + +class AlertSetting(Base): + """์•Œ๋ฆผ ์„ค์ •""" + __tablename__ = "alert_settings" + + id = Column(Integer, primary_key=True, index=True) + setting_key = Column(String(100), unique=True, nullable=False) + setting_value = Column(Text) # JSON ํ˜•์‹์œผ๋กœ ์ €์žฅ + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) diff --git a/asset_pilot_docker/docker-compose.yml b/asset_pilot_docker/docker-compose.yml new file mode 100644 index 0000000..b50e251 --- /dev/null +++ b/asset_pilot_docker/docker-compose.yml @@ -0,0 +1,67 @@ +services: + # 1. PostgreSQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค + postgres: + image: postgres:16-alpine + container_name: asset_pilot_db + restart: unless-stopped + environment: + POSTGRES_DB: asset_pilot + POSTGRES_USER: asset_user + POSTGRES_PASSWORD: ${DB_PASSWORD:-assetpilot} + POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=C" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./init-db:/docker-entrypoint-initdb.d + ports: + - "5432:5432" + networks: + - asset_pilot_network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U asset_user -d asset_pilot"] + interval: 10s + timeout: 5s + retries: 5 + + # 2. Asset Pilot ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ + app: + build: + context: . + dockerfile: Dockerfile + # DNS ์„ค์ • (๋นŒ๋“œ ๋ฐ–์œผ๋กœ ์ด๋™) + dns: + - 8.8.8.8 + - 1.1.1.1 + container_name: asset_pilot_app + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + environment: + # DB ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ธฐ๋ณธ๊ฐ’์„ postgres ์„œ๋น„์Šค์™€ ๋™์ผํ•˜๊ฒŒ 'assetpilot'์œผ๋กœ ์„ค์ • + DATABASE_URL: postgresql://asset_user:${DB_PASSWORD:-assetpilot}@postgres:5432/asset_pilot + APP_HOST: 0.0.0.0 + APP_PORT: 8000 + DEBUG: "False" + FETCH_INTERVAL: 5 + ports: + - "8000:8000" + networks: + - asset_pilot_network + volumes: + - app_logs:/app/logs + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +volumes: + postgres_data: + driver: local + app_logs: + driver: local + +networks: + asset_pilot_network: + driver: bridge \ No newline at end of file diff --git a/asset_pilot_docker/import_csv.py b/asset_pilot_docker/import_csv.py new file mode 100755 index 0000000..9903794 --- /dev/null +++ b/asset_pilot_docker/import_csv.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +""" +CSV ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ ์Šคํฌ๋ฆฝํŠธ (Docker์šฉ) +Windows ์•ฑ์˜ user_assets.csv ํŒŒ์ผ์„ PostgreSQL๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ +""" +import sys +import os +import csv +from sqlalchemy.orm import sessionmaker + +# app ๋ชจ๋“ˆ import +from app.database import engine +from app.models import Asset, UserAsset + +def import_csv(csv_file): + """CSV ํŒŒ์ผ์—์„œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ""" + if not os.path.exists(csv_file): + print(f"โŒ ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {csv_file}") + sys.exit(1) + + print(f"๐Ÿ“ CSV ํŒŒ์ผ ์ฝ๊ธฐ: {csv_file}") + + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + db = SessionLocal() + + try: + with open(csv_file, 'r', encoding='utf-8') as f: + reader = csv.reader(f) + count = 0 + + for row in reader: + if len(row) != 4: + print(f"โš ๏ธ ์ž˜๋ชป๋œ ํ–‰ ํ˜•์‹ (๋ฌด์‹œ): {row}") + continue + + asset_symbol, prev_close, avg_price, quantity = row + + # ์ž์‚ฐ ์ฐพ๊ธฐ + asset = db.query(Asset).filter(Asset.symbol == asset_symbol).first() + if not asset: + print(f"โš ๏ธ ์ž์‚ฐ์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ: {asset_symbol}") + continue + + # ์‚ฌ์šฉ์ž ์ž์‚ฐ ์—…๋ฐ์ดํŠธ + user_asset = db.query(UserAsset).filter(UserAsset.asset_id == asset.id).first() + if user_asset: + user_asset.previous_close = float(prev_close) + user_asset.average_price = float(avg_price) + user_asset.quantity = float(quantity) + count += 1 + print(f"โœ“ {asset_symbol}: ์ „์ผ={prev_close}, ํ‰๋‹จ={avg_price}, ์ˆ˜๋Ÿ‰={quantity}") + else: + print(f"โš ๏ธ ์‚ฌ์šฉ์ž ์ž์‚ฐ ์ •๋ณด ์—†์Œ: {asset_symbol}") + + db.commit() + print(f"\nโœ… {count}๊ฐœ ํ•ญ๋ชฉ ๊ฐ€์ ธ์˜ค๊ธฐ ์™„๋ฃŒ!") + + except Exception as e: + print(f"โŒ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}") + db.rollback() + finally: + db.close() + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("์‚ฌ์šฉ๋ฒ•: python import_csv.py ") + print("์˜ˆ์ œ: python import_csv.py user_assets.csv") + print("\nDocker์—์„œ ์‚ฌ์šฉ:") + print("1. docker cp user_assets.csv asset_pilot_app:/app/") + print("2. docker compose exec app python import_csv.py user_assets.csv") + sys.exit(1) + + csv_file = sys.argv[1] + import_csv(csv_file) diff --git a/asset_pilot_docker/init_db.py b/asset_pilot_docker/init_db.py new file mode 100755 index 0000000..d931588 --- /dev/null +++ b/asset_pilot_docker/init_db.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +""" +๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™” ์Šคํฌ๋ฆฝํŠธ +PostgreSQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•˜๊ณ  ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฝ์ž…ํ•ฉ๋‹ˆ๋‹ค. +""" +import sys +import os +from dotenv import load_dotenv +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +# ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ +load_dotenv() + +# app ๋ชจ๋“ˆ import๋ฅผ ์œ„ํ•œ ๊ฒฝ๋กœ ์ถ”๊ฐ€ +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from app.models import Base, Asset, UserAsset, AlertSetting +from app.database import DATABASE_URL +import json + +def init_database(): + """๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™”""" + print("๐Ÿ”ง ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™” ์‹œ์ž‘...") + + try: + # ์—”์ง„ ์ƒ์„ฑ + engine = create_engine(DATABASE_URL, echo=True) + + # ํ…Œ์ด๋ธ” ์ƒ์„ฑ + print("\n๐Ÿ“‹ ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์ค‘...") + Base.metadata.create_all(bind=engine) + print("โœ… ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์™„๋ฃŒ") + + # ์„ธ์…˜ ์ƒ์„ฑ + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + db = SessionLocal() + + try: + # ์ž์‚ฐ ๋งˆ์Šคํ„ฐ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” + print("\n๐Ÿ“Š ์ž์‚ฐ ๋งˆ์Šคํ„ฐ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์ค‘...") + assets_data = [ + ("XAU/USD", "๊ธˆ/๋‹ฌ๋Ÿฌ", "๊ท€๊ธˆ์†"), + ("XAU/CNY", "๊ธˆ/์œ„์•ˆ", "๊ท€๊ธˆ์†"), + ("XAU/GBP", "๊ธˆ/ํŒŒ์šด๋“œ", "๊ท€๊ธˆ์†"), + ("USD/DXY", "๋‹ฌ๋Ÿฌ์ธ๋ฑ์Šค", "ํ™˜์œจ"), + ("USD/KRW", "๋‹ฌ๋Ÿฌ/์›", "ํ™˜์œจ"), + ("BTC/USD", "๋น„ํŠธ์ฝ”์ธ/๋‹ฌ๋Ÿฌ", "์•”ํ˜ธํ™”ํ"), + ("BTC/KRW", "๋น„ํŠธ์ฝ”์ธ/์›", "์•”ํ˜ธํ™”ํ"), + ("KRX/GLD", "๊ธˆ ํ˜„๋ฌผ", "๊ท€๊ธˆ์†"), + ("XAU/KRW", "๊ธˆ/์›", "๊ท€๊ธˆ์†"), + ] + + for symbol, name, category in assets_data: + existing = db.query(Asset).filter(Asset.symbol == symbol).first() + if not existing: + asset = Asset(symbol=symbol, name=name, category=category) + db.add(asset) + print(f" โœ“ {symbol} ({name}) ์ถ”๊ฐ€") + else: + print(f" - {symbol} ์ด๋ฏธ ์กด์žฌ") + + db.commit() + print("โœ… ์ž์‚ฐ ๋งˆ์Šคํ„ฐ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ") + + # ์‚ฌ์šฉ์ž ์ž์‚ฐ ์ดˆ๊ธฐํ™” + print("\n๐Ÿ‘ค ์‚ฌ์šฉ์ž ์ž์‚ฐ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์ค‘...") + assets = db.query(Asset).all() + for asset in assets: + existing = db.query(UserAsset).filter(UserAsset.asset_id == asset.id).first() + if not existing: + user_asset = UserAsset( + asset_id=asset.id, + previous_close=0, + average_price=0, + quantity=0 + ) + db.add(user_asset) + print(f" โœ“ {asset.symbol} ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€") + else: + print(f" - {asset.symbol} ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ์ด๋ฏธ ์กด์žฌ") + + db.commit() + print("โœ… ์‚ฌ์šฉ์ž ์ž์‚ฐ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ") + + # ์•Œ๋ฆผ ์„ค์ • ์ดˆ๊ธฐํ™” + print("\n๐Ÿ”” ์•Œ๋ฆผ ์„ค์ • ์ดˆ๊ธฐํ™” ์ค‘...") + default_settings = { + "๊ธ‰๋“ฑ๋ฝ_๊ฐ์ง€": False, + "๊ธ‰๋“ฑ๋ฝ_์ž„๊ณ„๊ฐ’": 3.0, + "๋ชฉํ‘œ์ˆ˜์ต๋ฅ _๊ฐ์ง€": False, + "๋ชฉํ‘œ์ˆ˜์ต๋ฅ ": 10.0, + "ํŠน์ •๊ฐ€๊ฒฉ_๊ฐ์ง€": False, + "๊ธˆ_๋ชฉํ‘œ๊ฐ€๊ฒฉ": 100000, + "BTC_๋ชฉํ‘œ๊ฐ€๊ฒฉ": 100000000, + } + + for key, value in default_settings.items(): + existing = db.query(AlertSetting).filter(AlertSetting.setting_key == key).first() + if not existing: + setting = AlertSetting(setting_key=key, setting_value=json.dumps(value)) + db.add(setting) + print(f" โœ“ {key}: {value}") + else: + print(f" - {key} ์ด๋ฏธ ์กด์žฌ") + + db.commit() + print("โœ… ์•Œ๋ฆผ ์„ค์ • ์ดˆ๊ธฐํ™” ์™„๋ฃŒ") + + print("\n๐ŸŽ‰ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™” ์™„๋ฃŒ!") + print("\n๋‹ค์Œ ๋ช…๋ น์œผ๋กœ ์„œ๋ฒ„๋ฅผ ์‹œ์ž‘ํ•˜์„ธ์š”:") + print(" python main.py") + + finally: + db.close() + + except Exception as e: + print(f"\nโŒ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}") + print("\n๋ฌธ์ œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:") + print("1. PostgreSQL์ด ์‹คํ–‰ ์ค‘์ธ์ง€ ํ™•์ธํ•˜์„ธ์š”:") + print(" sudo systemctl status postgresql") + print("\n2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”:") + print(" sudo -u postgres psql -c '\\l'") + print("\n3. .env ํŒŒ์ผ์˜ DATABASE_URL์ด ์˜ฌ๋ฐ”๋ฅธ์ง€ ํ™•์ธํ•˜์„ธ์š”") + sys.exit(1) + +if __name__ == "__main__": + init_database() diff --git a/asset_pilot_docker/main.py b/asset_pilot_docker/main.py new file mode 100644 index 0000000..5475a47 --- /dev/null +++ b/asset_pilot_docker/main.py @@ -0,0 +1,268 @@ +import os +import json +import asyncio +from datetime import datetime +from typing import Dict +from fastapi import FastAPI, Depends, HTTPException, Request +from fastapi.responses import HTMLResponse, StreamingResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from sqlalchemy.orm import Session +from pydantic import BaseModel +from dotenv import load_dotenv + +from app.database import get_db, engine +from app.models import Base, Asset, UserAsset, AlertSetting +from app.fetcher import fetcher +from app.calculator import Calculator + +load_dotenv() + +# ํ…Œ์ด๋ธ” ์ƒ์„ฑ +Base.metadata.create_all(bind=engine) + +app = FastAPI(title="Asset Pilot - Orange Pi Edition", version="1.0.0") + +# ์ •์  ํŒŒ์ผ ๋ฐ ํ…œํ”Œ๋ฆฟ ์„ค์ • +app.mount("/static", StaticFiles(directory="static"), name="static") +templates = Jinja2Templates(directory="templates") + +# ์ „์—ญ ๋ณ€์ˆ˜: ํ˜„์žฌ ๊ฐ€๊ฒฉ ์บ์‹œ +current_prices: Dict = {} + +# ==================== Pydantic ๋ชจ๋ธ ==================== + +class UserAssetUpdate(BaseModel): + symbol: str + previous_close: float + average_price: float + quantity: float + +class AlertSettingUpdate(BaseModel): + settings: Dict + +# ==================== ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™” ==================== + +def init_assets(db: Session): + """์ž์‚ฐ ๋งˆ์Šคํ„ฐ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”""" + assets_data = [ + ("XAU/USD", "๊ธˆ/๋‹ฌ๋Ÿฌ", "๊ท€๊ธˆ์†"), + ("XAU/CNY", "๊ธˆ/์œ„์•ˆ", "๊ท€๊ธˆ์†"), + ("XAU/GBP", "๊ธˆ/ํŒŒ์šด๋“œ", "๊ท€๊ธˆ์†"), + ("USD/DXY", "๋‹ฌ๋Ÿฌ์ธ๋ฑ์Šค", "ํ™˜์œจ"), + ("USD/KRW", "๋‹ฌ๋Ÿฌ/์›", "ํ™˜์œจ"), + ("BTC/USD", "๋น„ํŠธ์ฝ”์ธ/๋‹ฌ๋Ÿฌ", "์•”ํ˜ธํ™”ํ"), + ("BTC/KRW", "๋น„ํŠธ์ฝ”์ธ/์›", "์•”ํ˜ธํ™”ํ"), + ("KRX/GLD", "๊ธˆ ํ˜„๋ฌผ", "๊ท€๊ธˆ์†"), + ("XAU/KRW", "๊ธˆ/์›", "๊ท€๊ธˆ์†"), + ] + + for symbol, name, category in assets_data: + existing = db.query(Asset).filter(Asset.symbol == symbol).first() + if not existing: + asset = Asset(symbol=symbol, name=name, category=category) + db.add(asset) + + db.commit() + print("โœ… ์ž์‚ฐ ๋งˆ์Šคํ„ฐ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ") + +def init_user_assets(db: Session): + """์‚ฌ์šฉ์ž ์ž์‚ฐ ์ดˆ๊ธฐํ™” (๊ธฐ๋ณธ๊ฐ’ 0)""" + assets = db.query(Asset).all() + for asset in assets: + existing = db.query(UserAsset).filter(UserAsset.asset_id == asset.id).first() + if not existing: + user_asset = UserAsset( + asset_id=asset.id, + previous_close=0, + average_price=0, + quantity=0 + ) + db.add(user_asset) + db.commit() + print("โœ… ์‚ฌ์šฉ์ž ์ž์‚ฐ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ") + +def init_alert_settings(db: Session): + """์•Œ๋ฆผ ์„ค์ • ์ดˆ๊ธฐํ™”""" + default_settings = { + "๊ธ‰๋“ฑ๋ฝ_๊ฐ์ง€": False, + "๊ธ‰๋“ฑ๋ฝ_์ž„๊ณ„๊ฐ’": 3.0, + "๋ชฉํ‘œ์ˆ˜์ต๋ฅ _๊ฐ์ง€": False, + "๋ชฉํ‘œ์ˆ˜์ต๋ฅ ": 10.0, + "ํŠน์ •๊ฐ€๊ฒฉ_๊ฐ์ง€": False, + "๊ธˆ_๋ชฉํ‘œ๊ฐ€๊ฒฉ": 100000, + "BTC_๋ชฉํ‘œ๊ฐ€๊ฒฉ": 100000000, + } + + for key, value in default_settings.items(): + existing = db.query(AlertSetting).filter(AlertSetting.setting_key == key).first() + if not existing: + setting = AlertSetting(setting_key=key, setting_value=json.dumps(value)) + db.add(setting) + db.commit() + print("โœ… ์•Œ๋ฆผ ์„ค์ • ์ดˆ๊ธฐํ™” ์™„๋ฃŒ") + +# ==================== ์•ฑ ์‹œ์ž‘ ์ด๋ฒคํŠธ ==================== + +@app.on_event("startup") +async def startup_event(): + """์•ฑ ์‹œ์ž‘ ์‹œ ์ดˆ๊ธฐํ™” ๋ฐ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ์‹œ์ž‘""" + db = next(get_db()) + try: + init_assets(db) + init_user_assets(db) + init_alert_settings(db) + print("๐Ÿš€ Asset Pilot ์„œ๋ฒ„ ์‹œ์ž‘ ์™„๋ฃŒ") + finally: + db.close() + + # ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์‹œ์ž‘ + asyncio.create_task(background_fetch()) + +async def background_fetch(): + """๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ฃผ๊ธฐ์ ์œผ๋กœ ๊ฐ€๊ฒฉ ์ˆ˜์ง‘""" + global current_prices + interval = int(os.getenv('FETCH_INTERVAL', 5)) + + while True: + try: + print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์‹œ์ž‘...") + current_prices = fetcher.fetch_all() + except Exception as e: + print(f"โŒ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์˜ค๋ฅ˜: {e}") + + await asyncio.sleep(interval) + +# ==================== API ์—”๋“œํฌ์ธํŠธ ==================== + +@app.get("/", response_class=HTMLResponse) +async def read_root(request: Request): + """๋ฉ”์ธ ํŽ˜์ด์ง€""" + return templates.TemplateResponse("index.html", {"request": request}) + +@app.get("/api/prices") +async def get_prices(): + """ํ˜„์žฌ ๊ฐ€๊ฒฉ ์กฐํšŒ""" + return current_prices + +@app.get("/api/assets") +async def get_assets(db: Session = Depends(get_db)): + """์‚ฌ์šฉ์ž ์ž์‚ฐ ์กฐํšŒ""" + assets = db.query(Asset, UserAsset).join( + UserAsset, Asset.id == UserAsset.asset_id + ).all() + + result = [] + for asset, user_asset in assets: + result.append({ + "symbol": asset.symbol, + "name": asset.name, + "category": asset.category, + "previous_close": float(user_asset.previous_close), + "average_price": float(user_asset.average_price), + "quantity": float(user_asset.quantity), + }) + + return result + +@app.post("/api/assets") +async def update_asset(data: UserAssetUpdate, db: Session = Depends(get_db)): + """์ž์‚ฐ ์ •๋ณด ์—…๋ฐ์ดํŠธ""" + asset = db.query(Asset).filter(Asset.symbol == data.symbol).first() + if not asset: + raise HTTPException(status_code=404, detail="์ž์‚ฐ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค") + + user_asset = db.query(UserAsset).filter(UserAsset.asset_id == asset.id).first() + if user_asset: + user_asset.previous_close = data.previous_close + user_asset.average_price = data.average_price + user_asset.quantity = data.quantity + db.commit() + return {"status": "success", "message": "์—…๋ฐ์ดํŠธ ์™„๋ฃŒ"} + + raise HTTPException(status_code=404, detail="์‚ฌ์šฉ์ž ์ž์‚ฐ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค") + +@app.get("/api/pnl") +async def get_pnl(db: Session = Depends(get_db)): + """์†์ต ๊ณ„์‚ฐ""" + # KRX/GLD ์ž์‚ฐ ์ •๋ณด + krx_asset = db.query(Asset).filter(Asset.symbol == "KRX/GLD").first() + krx_user = db.query(UserAsset).filter(UserAsset.asset_id == krx_asset.id).first() if krx_asset else None + + # BTC/KRW ์ž์‚ฐ ์ •๋ณด + btc_asset = db.query(Asset).filter(Asset.symbol == "BTC/KRW").first() + btc_user = db.query(UserAsset).filter(UserAsset.asset_id == btc_asset.id).first() if btc_asset else None + + gold_buy_price = float(krx_user.average_price) if krx_user else 0 + gold_quantity = float(krx_user.quantity) if krx_user else 0 + btc_buy_price = float(btc_user.average_price) if btc_user else 0 + btc_quantity = float(btc_user.quantity) if btc_user else 0 + + current_gold = current_prices.get("KRX/GLD", {}).get("๊ฐ€๊ฒฉ") + current_btc = current_prices.get("BTC/KRW", {}).get("๊ฐ€๊ฒฉ") + + pnl = Calculator.calc_pnl( + gold_buy_price, gold_quantity, + btc_buy_price, btc_quantity, + current_gold, current_btc + ) + + return pnl + +@app.get("/api/alerts/settings") +async def get_alert_settings(db: Session = Depends(get_db)): + """์•Œ๋ฆผ ์„ค์ • ์กฐํšŒ""" + settings = db.query(AlertSetting).all() + result = {} + for setting in settings: + try: + result[setting.setting_key] = json.loads(setting.setting_value) + except: + result[setting.setting_key] = setting.setting_value + return result + +@app.post("/api/alerts/settings") +async def update_alert_settings(data: AlertSettingUpdate, db: Session = Depends(get_db)): + """์•Œ๋ฆผ ์„ค์ • ์—…๋ฐ์ดํŠธ""" + for key, value in data.settings.items(): + setting = db.query(AlertSetting).filter(AlertSetting.setting_key == key).first() + if setting: + setting.setting_value = json.dumps(value) + else: + new_setting = AlertSetting(setting_key=key, setting_value=json.dumps(value)) + db.add(new_setting) + + db.commit() + return {"status": "success", "message": "์•Œ๋ฆผ ์„ค์ • ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ"} + +@app.get("/api/stream") +async def stream_prices(): + """Server-Sent Events๋กœ ์‹ค์‹œ๊ฐ„ ๊ฐ€๊ฒฉ ์ŠคํŠธ๋ฆฌ๋ฐ""" + async def event_generator(): + while True: + if current_prices: + data = json.dumps(current_prices, ensure_ascii=False) + yield f"data: {data}\n\n" + await asyncio.sleep(1) + + return StreamingResponse(event_generator(), media_type="text/event-stream") + +@app.get("/health") +async def health_check(): + """ํ—ฌ์Šค ์ฒดํฌ""" + return { + "status": "healthy", + "timestamp": datetime.now().isoformat(), + "prices_loaded": len(current_prices) > 0 + } + +# ==================== ๋ฉ”์ธ ์‹คํ–‰ ==================== + +if __name__ == "__main__": + import uvicorn + uvicorn.run( + "main:app", + host=os.getenv("APP_HOST", "0.0.0.0"), + port=int(os.getenv("APP_PORT", 8000)), + reload=os.getenv("DEBUG", "False").lower() == "true" + ) diff --git a/asset_pilot_docker/requirements.txt b/asset_pilot_docker/requirements.txt new file mode 100644 index 0000000..c85ccd3 --- /dev/null +++ b/asset_pilot_docker/requirements.txt @@ -0,0 +1,12 @@ +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +sqlalchemy==2.0.23 +psycopg2-binary==2.9.9 +pydantic==2.5.0 +python-dotenv==1.0.0 +jinja2==3.1.2 +aiofiles==23.2.1 +requests==2.31.0 +beautifulsoup4==4.12.2 +lxml==4.9.3 +python-multipart==0.0.6 diff --git a/asset_pilot_docker/start.sh b/asset_pilot_docker/start.sh new file mode 100755 index 0000000..4646082 --- /dev/null +++ b/asset_pilot_docker/start.sh @@ -0,0 +1,117 @@ +#!/bin/bash +# Asset Pilot Docker ๋น ๋ฅธ ์‹œ์ž‘ ์Šคํฌ๋ฆฝํŠธ + +set -e + +echo "๐Ÿณ Asset Pilot Docker ์„ค์น˜๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค..." +echo "" + +# ์ƒ‰์ƒ ์ •์˜ +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# Docker ์„ค์น˜ ํ™•์ธ +if ! command -v docker &> /dev/null; then + echo -e "${YELLOW}โš ๏ธ Docker๊ฐ€ ์„ค์น˜๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.${NC}" + echo "" + echo "Docker๋ฅผ ์„ค์น˜ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? (y/n)" + read -r INSTALL_DOCKER + + if [ "$INSTALL_DOCKER" = "y" ] || [ "$INSTALL_DOCKER" = "Y" ]; then + echo -e "${BLUE}๐Ÿณ Docker ์„ค์น˜ ์ค‘...${NC}" + curl -fsSL https://get.docker.com -o get-docker.sh + sudo sh get-docker.sh + sudo usermod -aG docker $USER + rm get-docker.sh + echo -e "${GREEN}โœ“ Docker ์„ค์น˜ ์™„๋ฃŒ${NC}" + echo "" + echo -e "${YELLOW}โš ๏ธ ๋กœ๊ทธ์•„์›ƒ ํ›„ ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•˜๊ฑฐ๋‚˜ ๋‹ค์Œ ๋ช…๋ น์„ ์‹คํ–‰ํ•˜์„ธ์š”:${NC}" + echo " newgrp docker" + echo "" + echo "๊ทธ๋Ÿฐ ๋‹ค์Œ ์ด ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋‹ค์‹œ ์‹คํ–‰ํ•˜์„ธ์š”." + exit 0 + else + echo -e "${RED}โŒ Docker๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์„ค์น˜ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š”.${NC}" + exit 1 + fi +fi + +echo -e "${GREEN}โœ“ Docker ํ™•์ธ ์™„๋ฃŒ${NC}" + +# Docker Compose ํ™•์ธ +if ! docker compose version &> /dev/null; then + echo -e "${YELLOW}โš ๏ธ Docker Compose๊ฐ€ ์„ค์น˜๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.${NC}" + echo -e "${BLUE}๐Ÿ“ฆ Docker Compose ์„ค์น˜ ์ค‘...${NC}" + sudo apt-get update + sudo apt-get install -y docker-compose-plugin + echo -e "${GREEN}โœ“ Docker Compose ์„ค์น˜ ์™„๋ฃŒ${NC}" +fi + +echo -e "${GREEN}โœ“ Docker Compose ํ™•์ธ ์™„๋ฃŒ${NC}" +echo "" + +# .env ํŒŒ์ผ ์„ค์ • +if [ ! -f ".env" ]; then + echo -e "${YELLOW}๐Ÿ” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์„ค์ •ํ•˜์„ธ์š”${NC}" + echo -n "๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ: " + read -s DB_PASSWORD + echo "" + + cat > .env << EOF +# PostgreSQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋น„๋ฐ€๋ฒˆํ˜ธ +DB_PASSWORD=${DB_PASSWORD} + +# ์„ ํƒ์  ์„ค์ • +# FETCH_INTERVAL=5 +# DEBUG=False +EOF + + chmod 600 .env + echo -e "${GREEN}โœ“ .env ํŒŒ์ผ ์ƒ์„ฑ ์™„๋ฃŒ${NC}" +else + echo -e "${YELLOW}โš ๏ธ .env ํŒŒ์ผ์ด ์ด๋ฏธ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค${NC}" +fi + +echo "" +echo -e "${BLUE}๐Ÿš€ Docker ์ปจํ…Œ์ด๋„ˆ ๋นŒ๋“œ ๋ฐ ์‹คํ–‰ ์ค‘...${NC}" +echo "" + +# Docker ์ด๋ฏธ์ง€ ๋นŒ๋“œ ๋ฐ ์ปจํ…Œ์ด๋„ˆ ์‹œ์ž‘ +docker compose up -d --build + +echo "" +echo -e "${BLUE}โณ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ค€๋น„ ๋Œ€๊ธฐ ์ค‘...${NC}" +sleep 10 + +# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™” +echo "" +echo -e "${BLUE}๐Ÿ”ง ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™” ์ค‘...${NC}" +docker compose exec app python init_db.py + +echo "" +echo -e "${GREEN}โœ… ์„ค์น˜ ์™„๋ฃŒ!${NC}" +echo "" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo -e "${GREEN}๐ŸŒ ์ ‘์† ์ •๋ณด${NC}" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + +# IP ์ฃผ์†Œ ํ™•์ธ +IP_ADDR=$(hostname -I | awk '{print $1}') +echo -e "๋กœ์ปฌ: ${BLUE}http://localhost:8000${NC}" +echo -e "๋„คํŠธ์›Œํฌ: ${BLUE}http://${IP_ADDR}:8000${NC}" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + +echo "" +echo "์œ ์šฉํ•œ ๋ช…๋ น์–ด:" +echo " docker compose ps # ์ปจํ…Œ์ด๋„ˆ ์ƒํƒœ ํ™•์ธ" +echo " docker compose logs -f # ๋กœ๊ทธ ์‹ค์‹œ๊ฐ„ ๋ณด๊ธฐ" +echo " docker compose restart # ์žฌ์‹œ์ž‘" +echo " docker compose down # ์ค‘์ง€" +echo "" +echo "CSV ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ:" +echo " docker cp user_assets.csv asset_pilot_app:/app/" +echo " docker compose exec app python import_csv.py user_assets.csv" +echo "" diff --git a/asset_pilot_docker/static/css/style.css b/asset_pilot_docker/static/css/style.css new file mode 100644 index 0000000..36bab9d --- /dev/null +++ b/asset_pilot_docker/static/css/style.css @@ -0,0 +1,353 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary-color: #2563eb; + --success-color: #10b981; + --danger-color: #ef4444; + --warning-color: #f59e0b; + --bg-color: #f8fafc; + --card-bg: #ffffff; + --text-primary: #1e293b; + --text-secondary: #64748b; + --border-color: #e2e8f0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background-color: var(--bg-color); + color: var(--text-primary); + line-height: 1.6; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 20px; +} + +/* ํ—ค๋” */ +header { + background: var(--card-bg); + border-radius: 12px; + padding: 24px; + margin-bottom: 24px; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +header h1 { + font-size: 32px; + margin-bottom: 8px; +} + +.subtitle { + color: var(--text-secondary); + font-size: 14px; + margin-bottom: 12px; +} + +.status-bar { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: var(--text-secondary); +} + +.status-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background-color: var(--success-color); + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +/* ์†์ต ์š”์•ฝ */ +.pnl-summary { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 16px; + margin-bottom: 24px; +} + +.pnl-card { + background: var(--card-bg); + border-radius: 12px; + padding: 20px; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +.pnl-card.total { + background: linear-gradient(135deg, var(--primary-color) 0%, #1e40af 100%); + color: white; +} + +.pnl-card h3 { + font-size: 14px; + font-weight: 500; + margin-bottom: 12px; + opacity: 0.9; +} + +.pnl-value { + font-size: 28px; + font-weight: 700; + margin-bottom: 4px; +} + +.pnl-percent { + font-size: 16px; + font-weight: 500; +} + +.pnl-value.profit { + color: var(--danger-color); +} + +.pnl-value.loss { + color: var(--primary-color); +} + +.pnl-card.total .pnl-value, +.pnl-card.total .pnl-percent { + color: white; +} + +/* ์ž์‚ฐ ์„น์…˜ */ +.assets-section { + background: var(--card-bg); + border-radius: 12px; + padding: 24px; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + margin-bottom: 24px; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.section-header h2 { + font-size: 20px; +} + +/* ํ…Œ์ด๋ธ” */ +.table-container { + overflow-x: auto; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th, td { + padding: 12px 16px; + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +th { + background-color: var(--bg-color); + font-weight: 600; + font-size: 14px; + color: var(--text-secondary); +} + +td { + font-size: 14px; +} + +.numeric { + text-align: right; +} + +td input { + width: 100%; + padding: 6px 8px; + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 14px; +} + +td input:focus { + outline: none; + border-color: var(--primary-color); +} + +.price-up { + color: var(--danger-color); +} + +.price-down { + color: var(--primary-color); +} + +/* ๋ฒ„ํŠผ */ +.btn { + padding: 10px 20px; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.btn-primary { + background-color: var(--primary-color); + color: white; +} + +.btn-primary:hover { + background-color: #1e40af; +} + +.btn-secondary { + background-color: var(--border-color); + color: var(--text-primary); +} + +.btn-secondary:hover { + background-color: #cbd5e1; +} + +/* ๋ชจ๋‹ฌ */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.5); +} + +.modal.active { + display: flex; + justify-content: center; + align-items: center; +} + +.modal-content { + background-color: var(--card-bg); + border-radius: 12px; + width: 90%; + max-width: 600px; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px; + border-bottom: 1px solid var(--border-color); +} + +.modal-header h2 { + font-size: 20px; +} + +.close { + font-size: 28px; + font-weight: 300; + cursor: pointer; + color: var(--text-secondary); +} + +.close:hover { + color: var(--text-primary); +} + +.modal-body { + padding: 24px; +} + +.setting-group { + margin-bottom: 24px; + padding-bottom: 24px; + border-bottom: 1px solid var(--border-color); +} + +.setting-group:last-child { + border-bottom: none; +} + +.setting-group h3 { + font-size: 16px; + margin-bottom: 16px; +} + +.setting-group label { + display: block; + margin-bottom: 12px; +} + +.setting-group input[type="checkbox"] { + margin-right: 8px; +} + +.setting-group input[type="number"] { + width: 120px; + padding: 8px; + border: 1px solid var(--border-color); + border-radius: 4px; + margin-left: 8px; +} + +.modal-footer { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 20px 24px; + border-top: 1px solid var(--border-color); +} + +/* ํ‘ธํ„ฐ */ +footer { + text-align: center; + padding: 24px; + color: var(--text-secondary); +} + +footer p { + margin-top: 12px; + font-size: 14px; +} + +/* ๋ฐ˜์‘ํ˜• */ +@media (max-width: 768px) { + .container { + padding: 12px; + } + + header h1 { + font-size: 24px; + } + + .pnl-summary { + grid-template-columns: 1fr; + } + + table { + font-size: 12px; + } + + th, td { + padding: 8px; + } +} diff --git a/asset_pilot_docker/static/js/app.js b/asset_pilot_docker/static/js/app.js new file mode 100644 index 0000000..231586a --- /dev/null +++ b/asset_pilot_docker/static/js/app.js @@ -0,0 +1,309 @@ +// ์ „์—ญ ๋ณ€์ˆ˜ +let currentPrices = {}; +let userAssets = []; +let alertSettings = {}; + +// ์ดˆ๊ธฐํ™” +document.addEventListener('DOMContentLoaded', () => { + loadAssets(); + loadAlertSettings(); + startPriceStream(); + + // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ + document.getElementById('refresh-btn').addEventListener('click', refreshData); + document.getElementById('alert-settings-btn').addEventListener('click', openAlertModal); + document.querySelector('.close').addEventListener('click', closeAlertModal); + document.getElementById('save-alerts').addEventListener('click', saveAlertSettings); + document.getElementById('cancel-alerts').addEventListener('click', closeAlertModal); + + // ์ฃผ๊ธฐ์  PnL ์—…๋ฐ์ดํŠธ + setInterval(updatePnL, 1000); +}); + +// ์ž์‚ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ +async function loadAssets() { + try { + const response = await fetch('/api/assets'); + userAssets = await response.json(); + renderAssetsTable(); + } catch (error) { + console.error('์ž์‚ฐ ๋กœ๋“œ ์‹คํŒจ:', error); + } +} + +// ์•Œ๋ฆผ ์„ค์ • ๋กœ๋“œ +async function loadAlertSettings() { + try { + const response = await fetch('/api/alerts/settings'); + alertSettings = await response.json(); + } catch (error) { + console.error('์•Œ๋ฆผ ์„ค์ • ๋กœ๋“œ ์‹คํŒจ:', error); + } +} + +// ์‹ค์‹œ๊ฐ„ ๊ฐ€๊ฒฉ ์ŠคํŠธ๋ฆฌ๋ฐ +function startPriceStream() { + const eventSource = new EventSource('/api/stream'); + + eventSource.onmessage = (event) => { + currentPrices = JSON.parse(event.data); + updatePricesInTable(); + updateLastUpdateTime(); + }; + + eventSource.onerror = () => { + console.error('SSE ์—ฐ๊ฒฐ ์˜ค๋ฅ˜'); + document.getElementById('status-indicator').style.backgroundColor = '#ef4444'; + }; +} + +// ํ…Œ์ด๋ธ” ๋ Œ๋”๋ง +function renderAssetsTable() { + const tbody = document.getElementById('assets-tbody'); + tbody.innerHTML = ''; + + const assets = [ + 'XAU/USD', 'XAU/CNY', 'XAU/GBP', 'USD/DXY', 'USD/KRW', + 'BTC/USD', 'BTC/KRW', 'KRX/GLD', 'XAU/KRW' + ]; + + assets.forEach(symbol => { + const asset = userAssets.find(a => a.symbol === symbol); + if (!asset) return; + + const row = document.createElement('tr'); + row.dataset.symbol = symbol; + + const decimalPlaces = symbol.includes('BTC') ? 8 : 2; + + row.innerHTML = ` + ${symbol} + + + + N/A + N/A + N/A + + + + + + + 0 + `; + + tbody.appendChild(row); + }); + + // ์ž…๋ ฅ ํ•„๋“œ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ + document.querySelectorAll('input[type="number"]').forEach(input => { + input.addEventListener('change', handleAssetChange); + input.addEventListener('blur', handleAssetChange); + }); +} + +// ํ…Œ์ด๋ธ”์— ๊ฐ€๊ฒฉ ์—…๋ฐ์ดํŠธ +function updatePricesInTable() { + const rows = document.querySelectorAll('#assets-tbody tr'); + + rows.forEach(row => { + const symbol = row.dataset.symbol; + const priceData = currentPrices[symbol]; + + if (!priceData || !priceData.๊ฐ€๊ฒฉ) { + return; + } + + const currentPrice = priceData.๊ฐ€๊ฒฉ; + const prevClose = parseFloat(row.querySelector('.prev-close').value) || 0; + + // ํ˜„์žฌ๊ฐ€ ํ‘œ์‹œ + const decimalPlaces = symbol.includes('USD') || symbol.includes('DXY') ? 2 : 0; + row.querySelector('.current-price').textContent = formatNumber(currentPrice, decimalPlaces); + + // ๋ณ€๋™ ๊ณ„์‚ฐ + const change = currentPrice - prevClose; + const changePercent = prevClose > 0 ? (change / prevClose * 100) : 0; + + const changeCell = row.querySelector('.change'); + const changePercentCell = row.querySelector('.change-percent'); + + changeCell.textContent = formatNumber(change, decimalPlaces); + changePercentCell.textContent = `${formatNumber(changePercent, 2)}%`; + + // ์ƒ‰์ƒ ์ ์šฉ + const colorClass = change > 0 ? 'price-up' : change < 0 ? 'price-down' : ''; + changeCell.className = `numeric ${colorClass}`; + changePercentCell.className = `numeric ${colorClass}`; + + // ๋งค์ž…์•ก ๊ณ„์‚ฐ + const avgPrice = parseFloat(row.querySelector('.avg-price').value) || 0; + const quantity = parseFloat(row.querySelector('.quantity').value) || 0; + const buyTotal = avgPrice * quantity; + row.querySelector('.buy-total').textContent = formatNumber(buyTotal, 0); + }); +} + +// ์†์ต ์—…๋ฐ์ดํŠธ +async function updatePnL() { + try { + const response = await fetch('/api/pnl'); + const pnl = await response.json(); + + // ๊ธˆ ์†์ต + updatePnLCard('gold-pnl', 'gold-percent', pnl.๊ธˆ์†์ต, pnl['๊ธˆ์†์ต%']); + + // BTC ์†์ต + updatePnLCard('btc-pnl', 'btc-percent', pnl.BTC์†์ต, pnl['BTC์†์ต%']); + + // ์ด ์†์ต + updatePnLCard('total-pnl', 'total-percent', pnl.์ด์†์ต, pnl['์ด์†์ต%']); + + } catch (error) { + console.error('PnL ์—…๋ฐ์ดํŠธ ์‹คํŒจ:', error); + } +} + +// PnL ์นด๋“œ ์—…๋ฐ์ดํŠธ +function updatePnLCard(valueId, percentId, value, percent) { + const valueElem = document.getElementById(valueId); + const percentElem = document.getElementById(percentId); + + valueElem.textContent = formatNumber(value, 0) + ' ์›'; + percentElem.textContent = formatNumber(percent, 2) + '%'; + + // ์ด์†์ต์ด ์•„๋‹Œ ๊ฒฝ์šฐ๋งŒ ์ƒ‰์ƒ ์ ์šฉ + if (valueId !== 'total-pnl') { + valueElem.className = `pnl-value ${value > 0 ? 'profit' : value < 0 ? 'loss' : ''}`; + percentElem.className = `pnl-percent ${value > 0 ? 'profit' : value < 0 ? 'loss' : ''}`; + } +} + +// ์ž์‚ฐ ๋ณ€๊ฒฝ ์ฒ˜๋ฆฌ +async function handleAssetChange(event) { + const input = event.target; + const symbol = input.dataset.symbol; + const row = input.closest('tr'); + + const previousClose = parseFloat(row.querySelector('.prev-close').value) || 0; + const averagePrice = parseFloat(row.querySelector('.avg-price').value) || 0; + const quantity = parseFloat(row.querySelector('.quantity').value) || 0; + + try { + const response = await fetch('/api/assets', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + symbol, + previous_close: previousClose, + average_price: averagePrice, + quantity: quantity + }) + }); + + if (response.ok) { + console.log(`โœ… ${symbol} ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ`); + // ๋งค์ž…์•ก ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธ + const buyTotal = averagePrice * quantity; + row.querySelector('.buy-total').textContent = formatNumber(buyTotal, 0); + } + } catch (error) { + console.error('์—…๋ฐ์ดํŠธ ์‹คํŒจ:', error); + } +} + +// ์•Œ๋ฆผ ์„ค์ • ๋ชจ๋‹ฌ ์—ด๊ธฐ +function openAlertModal() { + document.getElementById('๊ธ‰๋“ฑ๋ฝ_๊ฐ์ง€').checked = alertSettings.๊ธ‰๋“ฑ๋ฝ_๊ฐ์ง€ || false; + document.getElementById('๊ธ‰๋“ฑ๋ฝ_์ž„๊ณ„๊ฐ’').value = alertSettings.๊ธ‰๋“ฑ๋ฝ_์ž„๊ณ„๊ฐ’ || 3.0; + document.getElementById('๋ชฉํ‘œ์ˆ˜์ต๋ฅ _๊ฐ์ง€').checked = alertSettings.๋ชฉํ‘œ์ˆ˜์ต๋ฅ _๊ฐ์ง€ || false; + document.getElementById('๋ชฉํ‘œ์ˆ˜์ต๋ฅ ').value = alertSettings.๋ชฉํ‘œ์ˆ˜์ต๋ฅ  || 10.0; + document.getElementById('ํŠน์ •๊ฐ€๊ฒฉ_๊ฐ์ง€').checked = alertSettings.ํŠน์ •๊ฐ€๊ฒฉ_๊ฐ์ง€ || false; + document.getElementById('๊ธˆ_๋ชฉํ‘œ๊ฐ€๊ฒฉ').value = alertSettings.๊ธˆ_๋ชฉํ‘œ๊ฐ€๊ฒฉ || 100000; + document.getElementById('BTC_๋ชฉํ‘œ๊ฐ€๊ฒฉ').value = alertSettings.BTC_๋ชฉํ‘œ๊ฐ€๊ฒฉ || 100000000; + + document.getElementById('alert-modal').classList.add('active'); +} + +// ์•Œ๋ฆผ ์„ค์ • ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ +function closeAlertModal() { + document.getElementById('alert-modal').classList.remove('active'); +} + +// ์•Œ๋ฆผ ์„ค์ • ์ €์žฅ +async function saveAlertSettings() { + const settings = { + ๊ธ‰๋“ฑ๋ฝ_๊ฐ์ง€: document.getElementById('๊ธ‰๋“ฑ๋ฝ_๊ฐ์ง€').checked, + ๊ธ‰๋“ฑ๋ฝ_์ž„๊ณ„๊ฐ’: parseFloat(document.getElementById('๊ธ‰๋“ฑ๋ฝ_์ž„๊ณ„๊ฐ’').value), + ๋ชฉํ‘œ์ˆ˜์ต๋ฅ _๊ฐ์ง€: document.getElementById('๋ชฉํ‘œ์ˆ˜์ต๋ฅ _๊ฐ์ง€').checked, + ๋ชฉํ‘œ์ˆ˜์ต๋ฅ : parseFloat(document.getElementById('๋ชฉํ‘œ์ˆ˜์ต๋ฅ ').value), + ํŠน์ •๊ฐ€๊ฒฉ_๊ฐ์ง€: document.getElementById('ํŠน์ •๊ฐ€๊ฒฉ_๊ฐ์ง€').checked, + ๊ธˆ_๋ชฉํ‘œ๊ฐ€๊ฒฉ: parseInt(document.getElementById('๊ธˆ_๋ชฉํ‘œ๊ฐ€๊ฒฉ').value), + BTC_๋ชฉํ‘œ๊ฐ€๊ฒฉ: parseInt(document.getElementById('BTC_๋ชฉํ‘œ๊ฐ€๊ฒฉ').value) + }; + + try { + const response = await fetch('/api/alerts/settings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ settings }) + }); + + if (response.ok) { + alertSettings = settings; + console.log('โœ… ์•Œ๋ฆผ ์„ค์ • ์ €์žฅ ์™„๋ฃŒ'); + closeAlertModal(); + } + } catch (error) { + console.error('์•Œ๋ฆผ ์„ค์ • ์ €์žฅ ์‹คํŒจ:', error); + } +} + +// ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ +async function refreshData() { + await loadAssets(); + console.log('๐Ÿ”„ ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ ์™„๋ฃŒ'); +} + +// ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ ์‹œ๊ฐ„ ํ‘œ์‹œ +function updateLastUpdateTime() { + const now = new Date(); + const timeString = now.toLocaleTimeString('ko-KR'); + document.getElementById('last-update').textContent = `๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ: ${timeString}`; +} + +// ์ˆซ์ž ํฌ๋งทํŒ… +function formatNumber(value, decimals = 0) { + if (value === null || value === undefined || isNaN(value)) { + return 'N/A'; + } + return value.toLocaleString('ko-KR', { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals + }); +} + +// ์™ธ๋ถ€ ํด๋ฆญ ์‹œ ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ +window.addEventListener('click', (event) => { + const modal = document.getElementById('alert-modal'); + if (event.target === modal) { + closeAlertModal(); + } +}); diff --git a/asset_pilot_docker/templates/index.html b/asset_pilot_docker/templates/index.html new file mode 100644 index 0000000..0da0a6f --- /dev/null +++ b/asset_pilot_docker/templates/index.html @@ -0,0 +1,136 @@ + + + + + + Asset Pilot - ์ž์‚ฐ ๋ชจ๋‹ˆํ„ฐ + + + +
+
+

๐Ÿ’ฐ Asset Pilot

+

์‹ค์‹œ๊ฐ„ ์ž์‚ฐ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ

+
+ + ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์ค‘... +
+
+ +
+ +
+
+

๊ธˆ ํ˜„๋ฌผ

+
N/A
+
N/A
+
+
+

๋น„ํŠธ์ฝ”์ธ

+
N/A
+
N/A
+
+
+

์ด ์†์ต

+
N/A
+
N/A
+
+
+ + +
+
+

๐Ÿ“Š ์ž์‚ฐ ํ˜„ํ™ฉ

+ +
+ +
+ + + + + + + + + + + + + + + + +
ํ•ญ๋ชฉ์ „์ผ์ข…๊ฐ€ํ˜„์žฌ๊ฐ€๋ณ€๋™๋ณ€๋™๋ฅ ํ‰๋‹จ๊ฐ€๋ณด์œ ๋Ÿ‰๋งค์ž…์•ก
+
+
+ + + +
+ +
+ +

Asset Pilot v1.0 - Orange Pi Edition

+
+
+ + + +