diff --git a/README.md b/README.md index e34d65c..1b1e7f2 100644 --- a/README.md +++ b/README.md @@ -1,262 +1,334 @@ -# ๐Ÿ  Home NAS Web Platform (webpage) +# web-backend -Synology NAS ๊ธฐ๋ฐ˜์˜ ๊ฐœ์ธ ์›น ํ”Œ๋žซํผ์œผ๋กœ, ๋กœ๋˜ ๋ถ„์„/์ถ”์ฒœ ์„œ๋น„์Šค์™€ ์—ฌํ–‰ ์‚ฌ์ง„ ์ง€๋„ ์„œ๋น„์Šค๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. -Frontend, Backend, Travel Proxy, Auto Deployer๋ฅผ Docker Compose๋กœ ํ†ตํ•ฉํ•˜์—ฌ ์šด์˜ํ•ฉ๋‹ˆ๋‹ค. +Synology NAS ๊ธฐ๋ฐ˜ ๊ฐœ์ธ ์›น ํ”Œ๋žซํผ ๋ฐฑ์—”๋“œ ๋ชจ๋…ธ๋ ˆํฌ. +๋กœ๋˜ ๋ถ„์„, ์ฃผ์‹ ํฌํŠธํด๋ฆฌ์˜ค, ์—ฌํ–‰ ์•จ๋ฒ”, ๋ธ”๋กœ๊ทธ, ํˆฌ๋‘๋ฆฌ์ŠคํŠธ๋ฅผ ํ•˜๋‚˜์˜ ์„œ๋น„์Šค๋กœ ์šด์˜ํ•œ๋‹ค. --- -## ๐Ÿ–ฅ๏ธ NAS ํ™˜๊ฒฝ ์ •๋ณด - -| ํ•ญ๋ชฉ | ๊ฐ’ | -| --------------- | ------------------------------------- | -| **NAS** | Synology NAS | -| **OS** | Synology DSM | -| **CPU** | Intel Celeron J4025 (2 Core, 2.0GHz) | -| **๋ฉ”๋ชจ๋ฆฌ** | 18 GB | -| **Docker** | Synology Container Manager | -| **Reverse Proxy** | Nginx (์ปจํ…Œ์ด๋„ˆ) | -| **Git Server** | Gitea (self-hosted) | - ---- - -## ๐Ÿ“ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ +## ์„œ๋น„์Šค ๊ตฌ์„ฑ ``` -/volume1 -โ”œโ”€โ”€ docker/ -โ”‚ โ””โ”€โ”€ webpage/ # ๐Ÿš€ ์šด์˜ ๋Ÿฐํƒ€์ž„ (Docker Compose ๊ธฐ์ค€์ ) -โ”‚ โ”œโ”€โ”€ backend/ # lotto-backend -โ”‚ โ”œโ”€โ”€ stock-lab/ # ๐ŸŸช stock-lab (Stock + AI) -โ”‚ โ”œโ”€โ”€ travel-proxy/ # travel API + thumbnail generator -โ”‚ โ”œโ”€โ”€ deployer/ # webhook ๊ธฐ๋ฐ˜ ์ž๋™ ๋ฐฐํฌ ์ปจํ…Œ์ด๋„ˆ -โ”‚ โ”œโ”€โ”€ frontend/ # ์ •์  ํŒŒ์ผ (Vite build ๊ฒฐ๊ณผ) -โ”‚ โ”œโ”€โ”€ nginx/ -โ”‚ โ”‚ โ””โ”€โ”€ default.conf -โ”‚ โ”œโ”€โ”€ scripts/ -โ”‚ โ”‚ โ””โ”€โ”€ deploy.sh # webhook์ด ํ˜ธ์ถœํ•˜๋Š” ์‹คํ–‰๊ธฐ -โ”‚ โ”œโ”€โ”€ docker-compose.yml -โ”‚ โ””โ”€โ”€ data/ -โ”‚ โ””โ”€โ”€ lotto.db +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ lotto-frontend (Nginx:8080) โ”‚ +โ”‚ โ”œโ”€โ”€ ์ •์  SPA ์„œ๋น™ (React + Vite) โ”‚ +โ”‚ โ””โ”€โ”€ API ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ โ”‚ +โ”‚ โ”œโ”€โ”€ /api/ โ†’ lotto-backend:8000 โ”‚ +โ”‚ โ”œโ”€โ”€ /api/stock/ โ†’ stock-lab:8000 โ”‚ +โ”‚ โ”œโ”€โ”€ /api/trade/ โ†’ stock-lab:8000 โ”‚ +โ”‚ โ”œโ”€โ”€ /api/portfolio โ†’ stock-lab:8000 โ”‚ +โ”‚ โ”œโ”€โ”€ /api/travel/ โ†’ travel-proxy:8000 โ”‚ +โ”‚ โ””โ”€โ”€ /webhook โ†’ deployer:9000 โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +| ์ปจํ…Œ์ด๋„ˆ | ํฌํŠธ | ์—ญํ•  | +|---------|------|------| +| `lotto-backend` | 18000 | ๋กœ๋˜ยท๋ธ”๋กœ๊ทธยทํˆฌ๋‘ API | +| `stock-lab` | 18500 | ์ฃผ์‹ ๋‰ด์ŠคยทํฌํŠธํด๋ฆฌ์˜คยท์ž์‚ฐ ์ถ”์  | +| `travel-proxy` | 19000 | ์—ฌํ–‰ ์‚ฌ์ง„ API + ์ธ๋„ค์ผ ์ƒ์„ฑ | +| `lotto-frontend` | 8080 | SPA ์„œ๋น™ + ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ | +| `webpage-deployer` | 19010 | Gitea Webhook โ†’ ์ž๋™ ๋ฐฐํฌ | + +--- + +## ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ + +``` +web-backend/ +โ”œโ”€โ”€ backend/ # lotto-backend ์„œ๋น„์Šค (Python/FastAPI) +โ”‚ โ”œโ”€โ”€ app/ +โ”‚ โ”‚ โ”œโ”€โ”€ main.py # ๋ผ์šฐํ„ฐ, ์Šค์ผ€์ค„๋Ÿฌ +โ”‚ โ”‚ โ”œโ”€โ”€ db.py # SQLite CRUD (7๊ฐœ ํ…Œ์ด๋ธ”) +โ”‚ โ”‚ โ”œโ”€โ”€ generator.py # ๋ชฌํ…Œ์นด๋ฅผ๋กœ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์—”์ง„ +โ”‚ โ”‚ โ”œโ”€โ”€ analyzer.py # 5๊ฐ€์ง€ ํ†ต๊ณ„ ๋ถ„์„ +โ”‚ โ”‚ โ”œโ”€โ”€ checker.py # ๋‹น์ฒจ ๊ฒฐ๊ณผ ์ฑ„์  +โ”‚ โ”‚ โ”œโ”€โ”€ collector.py # ๋กœ๋˜ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ +โ”‚ โ”‚ โ”œโ”€โ”€ recommender.py # ์ถ”์ฒœ ์•Œ๊ณ ๋ฆฌ์ฆ˜ +โ”‚ โ”‚ โ””โ”€โ”€ utils.py # ๋ฉ”ํŠธ๋ฆญ ๊ณ„์‚ฐ +โ”‚ โ””โ”€โ”€ Dockerfile โ”‚ -โ”œโ”€โ”€ workspace/ -โ”‚ โ””โ”€โ”€ web-page-backend/ # ๐Ÿง  Git ๋ ˆํฌ (backend + infra) -โ”‚ โ”œโ”€โ”€ backend/ -โ”‚ โ”œโ”€โ”€ travel-proxy/ -โ”‚ โ”œโ”€โ”€ deployer/ -โ”‚ โ”œโ”€โ”€ nginx/ -โ”‚ โ”œโ”€โ”€ scripts/ -โ”‚ โ”‚ โ””โ”€โ”€ deploy-nas.sh # ์‹ค์ œ ์šด์˜ ๋ฐ˜์˜ ๋กœ์ง -โ”‚ โ”œโ”€โ”€ docker-compose.yml -โ”‚ โ”œโ”€โ”€ .env.example -โ”‚ โ””โ”€โ”€ README.md +โ”œโ”€โ”€ stock-lab/ # stock-lab ์„œ๋น„์Šค (Python/FastAPI) +โ”‚ โ”œโ”€โ”€ app/ +โ”‚ โ”‚ โ”œโ”€โ”€ main.py # ๋ผ์šฐํ„ฐ, ์Šค์ผ€์ค„๋Ÿฌ +โ”‚ โ”‚ โ”œโ”€โ”€ db.py # SQLite CRUD (4๊ฐœ ํ…Œ์ด๋ธ”) +โ”‚ โ”‚ โ”œโ”€โ”€ scraper.py # ๋„ค์ด๋ฒ„ ๊ธˆ์œต ๋‰ด์Šค ํฌ๋กค๋ง +โ”‚ โ”‚ โ”œโ”€โ”€ price_fetcher.py # ํ˜„์žฌ๊ฐ€ ์กฐํšŒ (3๋ถ„ ์บ์‹œ) +โ”‚ โ”‚ โ””โ”€โ”€ holidays.json # ํ•œ๊ตญ ์ฃผ์‹์‹œ์žฅ ํœด์žฅ์ผ +โ”‚ โ””โ”€โ”€ Dockerfile โ”‚ -โ””โ”€โ”€ web/ - โ””โ”€โ”€ images/ - โ””โ”€โ”€ webPage/ - โ””โ”€โ”€ travel/ # ๐Ÿ“ท ์›๋ณธ ์—ฌํ–‰ ์‚ฌ์ง„ (RO) +โ”œโ”€โ”€ travel-proxy/ # travel-proxy ์„œ๋น„์Šค (Python/FastAPI) +โ”‚ โ”œโ”€โ”€ app/ +โ”‚ โ”‚ โ””โ”€โ”€ main.py # ์‚ฌ์ง„ API, ์ธ๋„ค์ผ ์ƒ์„ฑ (Pillow) +โ”‚ โ””โ”€โ”€ Dockerfile +โ”‚ +โ”œโ”€โ”€ deployer/ # Gitea Webhook ์ˆ˜์‹  โ†’ ์ž๋™ ๋ฐฐํฌ +โ”‚ โ”œโ”€โ”€ app.py # HMAC SHA256 ๊ฒ€์ฆ + ๋ฐฐํฌ ํŠธ๋ฆฌ๊ฑฐ +โ”‚ โ””โ”€โ”€ Dockerfile +โ”‚ +โ”œโ”€โ”€ nginx/ +โ”‚ โ””โ”€โ”€ default.conf # ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ + SPA + ์บ์‹œ +โ”‚ +โ”œโ”€โ”€ scripts/ +โ”‚ โ”œโ”€โ”€ deploy.sh # ์šด์˜ ๋ฐฐํฌ (git pull โ†’ rsync โ†’ compose up) +โ”‚ โ”œโ”€โ”€ deploy-nas.sh # rsync ์ „์šฉ ์Šคํฌ๋ฆฝํŠธ +โ”‚ โ””โ”€โ”€ healthcheck.sh # ์ „์ฒด ์„œ๋น„์Šค ํ—ฌ์Šค ์ฒดํฌ +โ”‚ +โ”œโ”€โ”€ docker-compose.yml +โ”œโ”€โ”€ .env.example +โ””โ”€โ”€ CLAUDE.md ``` --- -## ๐Ÿงฉ ์„œ๋น„์Šค ๊ตฌ์„ฑ ๊ฐœ์š” +## ๋น ๋ฅธ ์‹œ์ž‘ (๋กœ์ปฌ ๊ฐœ๋ฐœ) -```mermaid -graph LR - User[User Browser] -->|HTTP| Nginx - - subgraph NAS [Synology NAS] - Nginx -->|/api/lotto| Lotto[Lotto Backend] - Nginx -->|/api/travel| Travel[Travel Proxy] - Nginx -->|/api/stock| Stock[Stock Lab] - Stock -->|Trading/Balance| KIS[KIS API (Korea Inv.)] - Stock -->|Market News| News[News Sites] - end - - subgraph Windows [High-Performance PC] - Stock -->|Analyze Request| WinServer[Windows AI Server] - WinServer -->|LLM Inference| Ollama[Ollama (Llama 3.1)] - WinServer -->|GPU| GPU[NVIDIA 3070 Ti] - end -``` - ---- - -## ๐Ÿ› ๏ธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ค์ • (Local Development) - -์ด ํ”„๋กœ์ ํŠธ๋Š” **Windows/Mac ๋กœ์ปฌ ํ™˜๊ฒฝ**๊ณผ **Synology NAS ์šด์˜ ํ™˜๊ฒฝ**์„ ๋ชจ๋‘ ์ง€์›ํ•˜๋„๋ก ๊ตฌ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. - -### 1. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • -`docker-compose.yml`์€ ํ™˜๊ฒฝ ๋ณ€์ˆ˜์— ์˜์กดํ•ฉ๋‹ˆ๋‹ค. -1. `.env.example` ํŒŒ์ผ์„ ๋ณต์‚ฌํ•˜์—ฌ `.env` ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜์„ธ์š”. - ```bash - cp .env.example .env - ``` -2. `.env` ํŒŒ์ผ์˜ ๊ฒฝ๋กœ(`RUNTIME_PATH`, `PHOTO_PATH` ๋“ฑ)๋ฅผ ๋กœ์ปฌ ํ™˜๊ฒฝ์— ๋งž๊ฒŒ ์ˆ˜์ •ํ•˜์„ธ์š”. - - ๊ธฐ๋ณธ๊ฐ’์€ ํ˜„์žฌ ๋””๋ ‰ํ† ๋ฆฌ(`.`) ๊ธฐ์ค€์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ์–ด ๋ฐ”๋กœ ์‹คํ–‰ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. - -### 2. ๋กœ์ปฌ ์‹คํ–‰ ```bash +# 1. ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ • +cp .env.example .env + +# 2. ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰ (.env ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ฆ‰์‹œ ์‹คํ–‰ ๊ฐ€๋Šฅ) docker compose up -d + +# 3. ํ™•์ธ +curl http://localhost:18000/health +curl http://localhost:18500/health ``` -- Frontend: http://localhost:8080 -- Backend API: http://localhost:18000 -- Travel API: http://localhost:19000 -- Stock Lab API: http://localhost:18500 + +| ์„œ๋น„์Šค | ๋กœ์ปฌ URL | +|--------|----------| +| Frontend + API | http://localhost:8080 | +| lotto-backend | http://localhost:18000 | +| stock-lab | http://localhost:18500 | +| travel-proxy | http://localhost:19000 | --- -### ๐ŸŸฆ Frontend (lotto-frontend) +## API ๋ชฉ๋ก -- **์—ญํ• **: React + Vite ๊ธฐ๋ฐ˜ SPA๋กœ, ๋กœ๋˜ ์ถ”์ฒœ ๋ฐ ์—ฌํ–‰ ์ง€๋„ UI๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. -- **ํŠน์ง•**: - - ์ •์  ํŒŒ์ผ๋งŒ ์šด์˜ ์„œ๋ฒ„์— ๋ฐฐํฌํ•ฉ๋‹ˆ๋‹ค. - - ์žฅ๊ธฐ ์บ์‹œ(`assets/`)์™€ `index.html` ์บ์‹œ ๋ฌดํšจํ™” ์ „๋žต์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. - - Backend / Travel API๋Š” Nginx์—์„œ Reverse Proxy๋กœ ์—ฐ๊ฒฐ๋ฉ๋‹ˆ๋‹ค. -- **๋ฐฐํฌ ๋ฐฉ์‹**: - 1. **๋กœ์ปฌ ๊ฐœ๋ฐœ**: - - `.env` ํŒŒ์ผ ์„ค์ • ํ›„ `docker compose up`์œผ๋กœ ์ „์ฒด ์Šคํƒ ์‹คํ–‰ ๊ฐ€๋Šฅ - 2. **์šด์˜ ๋ฐฐํฌ**: - - Code๋ฅผ Git์— Push - - Webhook์ด ํŠธ๋ฆฌ๊ฑฐ๋˜์–ด NAS๊ฐ€ ์ž๋™ Pull & Deploy - - (Frontend ๋นŒ๋“œ ์‚ฐ์ถœ๋ฌผ์€ ๋ณ„๋„ ์—…๋กœ๋“œ ํ˜น์€ CI ์—ฐ๋™ ํ•„์š”) +### lotto-backend (`/api/`) + +#### ๋กœ๋˜ + +| ๋ฉ”์„œ๋“œ | ๊ฒฝ๋กœ | ์„ค๋ช… | +|--------|------|------| +| GET | `/api/lotto/latest` | ์ตœ์‹  ๋‹น์ฒจ๋ฒˆํ˜ธ | +| GET | `/api/lotto/{drw_no}` | ํŠน์ • ํšŒ์ฐจ | +| GET | `/api/lotto/stats` | ๋ฒˆํ˜ธ ๋นˆ๋„ ํ†ต๊ณ„ | +| GET | `/api/lotto/analysis` | 5๊ฐ€์ง€ ํ†ต๊ณ„ ๋ถ„์„ ๋ฆฌํฌํŠธ | +| GET | `/api/lotto/best` | ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ตœ์  ๋ฒˆํ˜ธ (๊ธฐ๋ณธ 20์Œ) | +| GET | `/api/lotto/simulation` | ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ƒ์„ธ ๊ฒฐ๊ณผ | +| GET | `/api/lotto/recommend` | ํ†ต๊ณ„ ๊ธฐ๋ฐ˜ ์ถ”์ฒœ | +| GET | `/api/lotto/recommend/heatmap` | ํžˆํŠธ๋งต ๊ธฐ๋ฐ˜ ์ถ”์ฒœ | +| GET | `/api/lotto/recommend/batch` | ๋ฐฐ์น˜ ์ถ”์ฒœ | +| POST | `/api/admin/simulate` | ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ˆ˜๋™ ์‹คํ–‰ | +| POST | `/api/admin/sync_latest` | ๋‹น์ฒจ๋ฒˆํ˜ธ ์ˆ˜๋™ ๋™๊ธฐํ™” | + +#### ์ถ”์ฒœ ์ด๋ ฅ + +| ๋ฉ”์„œ๋“œ | ๊ฒฝ๋กœ | ์„ค๋ช… | +|--------|------|------| +| GET | `/api/history` | ๋ชฉ๋ก (limit, offset, favorite, tag, sort) | +| PATCH | `/api/history/{id}` | ์ฆ๊ฒจ์ฐพ๊ธฐยท๋ฉ”๋ชจยทํƒœ๊ทธ ์ˆ˜์ • | +| DELETE | `/api/history/{id}` | ์‚ญ์ œ | + +#### ํˆฌ๋‘๋ฆฌ์ŠคํŠธ + +| ๋ฉ”์„œ๋“œ | ๊ฒฝ๋กœ | ์„ค๋ช… | +|--------|------|------| +| GET | `/api/todos` | ์ „์ฒด ๋ชฉ๋ก | +| POST | `/api/todos` | ์ƒ์„ฑ (status: todo\|in_progress\|done) | +| PUT | `/api/todos/{id}` | ์ˆ˜์ • | +| DELETE | `/api/todos/done` | ์™„๋ฃŒ ํ•ญ๋ชฉ ์ผ๊ด„ ์‚ญ์ œ | +| DELETE | `/api/todos/{id}` | ๊ฐœ๋ณ„ ์‚ญ์ œ | + +> โš ๏ธ `/done` ๋ผ์šฐํŠธ๋Š” ๋ฐ˜๋“œ์‹œ `/{id}` ๋ณด๋‹ค ๋จผ์ € ๋“ฑ๋กํ•ด์•ผ ํ•จ + +#### ๋ธ”๋กœ๊ทธ + +| ๋ฉ”์„œ๋“œ | ๊ฒฝ๋กœ | ์„ค๋ช… | +|--------|------|------| +| GET | `/api/blog/posts` | ๊ธ€ ๋ชฉ๋ก (`{"posts": [...]}`, date DESC) | +| POST | `/api/blog/posts` | ๊ธ€ ์ƒ์„ฑ (date ๋ฏธ์ž…๋ ฅ ์‹œ ์˜ค๋Š˜ ๋‚ ์งœ) | +| PUT | `/api/blog/posts/{id}` | ๊ธ€ ์ˆ˜์ • | +| DELETE | `/api/blog/posts/{id}` | ๊ธ€ ์‚ญ์ œ | + +๋ธ”๋กœ๊ทธ ํฌ์ŠคํŠธ ๊ตฌ์กฐ: `{ id, title, tags[], body, date, excerpt, created_at, updated_at }` --- -### ๐ŸŸฉ Backend (lotto-backend) +### stock-lab (`/api/stock/`, `/api/trade/`, `/api/portfolio`) -- **์—ญํ• **: ๋กœ๋˜ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘, ๋ถ„์„, ์ถ”์ฒœ API๋ฅผ ์ œ๊ณตํ•˜๋ฉฐ SQLite๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. -- **์ฃผ์š” ๊ธฐ๋Šฅ**: - - ์ตœ์‹  ๋ฐ ํŠน์ • ํšŒ์ฐจ ์กฐํšŒ - - ์ถ”์ฒœ ๋ฒˆํ˜ธ ์ƒ์„ฑ ๋ฐ ํžˆ์Šคํ† ๋ฆฌ ๊ด€๋ฆฌ (์ค‘๋ณต ์ œ๊ฑฐ) - - ์ฆ๊ฒจ์ฐพ๊ธฐ, ๋ฉ”๋ชจ, ํƒœ๊ทธ ๊ด€๋ฆฌ - - ๋ฐฐ์น˜ ์ถ”์ฒœ ๊ธฐ๋Šฅ -- **๊ธฐ์ˆ  ์Šคํƒ**: FastAPI, SQLite, APScheduler (์ •๊ธฐ ์ˆ˜์ง‘) -- **์ฃผ์š” ์—”๋“œํฌ์ธํŠธ**: - ```http - GET /api/lotto/latest - GET /api/lotto/{drw_no} - GET /api/lotto/recommend - GET /api/lotto/recommend/batch - GET /api/history - PATCH /api/history/{id} - DELETE /api/history/{id} - ``` +#### ๋‰ด์Šค & ์ง€ํ‘œ + +| ๋ฉ”์„œ๋“œ | ๊ฒฝ๋กœ | ์„ค๋ช… | +|--------|------|------| +| GET | `/api/stock/news` | ๋‰ด์Šค ๋ชฉ๋ก (limit, category) | +| GET | `/api/stock/indices` | ์ฃผ์š” ์ง€ํ‘œ (KOSPI ๋“ฑ) | +| POST | `/api/stock/scrap` | ๋‰ด์Šค ์ˆ˜๋™ ์Šคํฌ๋žฉ | + +#### ์‹ค๊ณ„์ขŒ (Windows AI ์„œ๋ฒ„ ํ”„๋ก์‹œ) + +| ๋ฉ”์„œ๋“œ | ๊ฒฝ๋กœ | ์„ค๋ช… | +|--------|------|------| +| GET | `/api/trade/balance` | ์‹ค๊ณ„์ขŒ ์ž”๊ณ  ์กฐํšŒ | +| POST | `/api/trade/order` | ์ฃผ๋ฌธ (BUY\|SELL, price=0์ด๋ฉด ์‹œ์žฅ๊ฐ€) | + +#### ํฌํŠธํด๋ฆฌ์˜ค + +| ๋ฉ”์„œ๋“œ | ๊ฒฝ๋กœ | ์„ค๋ช… | +|--------|------|------| +| GET | `/api/portfolio` | ์ „์ฒด ์กฐํšŒ (ํ˜„์žฌ๊ฐ€ยท์†์ตยท์˜ˆ์ˆ˜๊ธˆ ํฌํ•จ) | +| POST | `/api/portfolio` | ์ข…๋ชฉ ์ถ”๊ฐ€ | +| PUT | `/api/portfolio/{id}` | ์ข…๋ชฉ ์ˆ˜์ • | +| DELETE | `/api/portfolio/{id}` | ์ข…๋ชฉ ์‚ญ์ œ | +| GET | `/api/portfolio/cash` | ์˜ˆ์ˆ˜๊ธˆ ์ „์ฒด ์กฐํšŒ | +| PUT | `/api/portfolio/cash` | ์˜ˆ์ˆ˜๊ธˆ upsert | +| DELETE | `/api/portfolio/cash/{broker}` | ์˜ˆ์ˆ˜๊ธˆ ์‚ญ์ œ | +| POST | `/api/portfolio/snapshot` | ์ด ์ž์‚ฐ ์Šค๋ƒ…์ƒท ์ˆ˜๋™ ์ €์žฅ | +| GET | `/api/portfolio/snapshot/history` | ์ž์‚ฐ ๋ณ€ํ™” ์ด๋ ฅ (days=0: ์ „์ฒด) | --- -### ๐ŸŸช Stock Lab (stock-lab) +### travel-proxy (`/api/travel/`) -- **์—ญํ• **: ์ฃผ์‹ ์‹œ์žฅ ๋ถ„์„ ๋ฐ AI ๊ธฐ๋ฐ˜ ํˆฌ์ž ์กฐ์–ธ ์ œ๊ณต. NAS์˜ ํŽธ์˜์„ฑ๊ณผ Windows PC์˜ ๊ณ ์„ฑ๋Šฅ์„ ๊ฒฐํ•ฉํ•œ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•„ํ‚คํ…์ฒ˜์ž…๋‹ˆ๋‹ค. -- **์ฃผ์š” ๊ธฐ๋Šฅ**: - - **์‹œ์žฅ ๋‰ด์Šค ์Šคํฌ๋žฉ**: ๋„ค์ด๋ฒ„ ์ฆ๊ถŒ, ํ•ด์™ธ ์ฃผ์š” ๋‰ด์Šค ์‚ฌ์ดํŠธ ํฌ๋กค๋ง - - **์ž์‚ฐ ๊ด€๋ฆฌ**: ํ•œ๊ตญํˆฌ์ž์ฆ๊ถŒ(KIS) Open API ์—ฐ๋™ (์ž”๊ณ  ์กฐํšŒ, ๋งค์ˆ˜/๋งค๋„) - - **AI ๋ถ„์„**: ๊ณ ์„ฑ๋Šฅ PC์˜ ๋กœ์ปฌ LLM(Llama 3.1)์„ ํ™œ์šฉํ•˜์—ฌ ๋‰ด์Šค+ํฌํŠธํด๋ฆฌ์˜ค ์ข…ํ•ฉ ๋ถ„์„ -- **๊ธฐ์ˆ  ์Šคํƒ**: Python, FastAPI, Ollama (Lava/Llama3), Docker -- **์—ฐ๋™ ๊ตฌ์กฐ**: - ``` - [NAS Stock-Lab] <--(HTTP)--> [Windows AI Server] <--(Localhost)--> [Ollama GPU] - ``` -- **์ฃผ์š” ์—”๋“œํฌ์ธํŠธ**: - ```http - GET /api/stock/analyze # AI ์‹œ์žฅ ๋ถ„์„ ์š”์ฒญ - GET /api/stock/news # ์ตœ์‹  ๋‰ด์Šค ๋ฐ์ดํ„ฐ - GET /api/trade/balance # ๊ณ„์ขŒ ์ž”๊ณ  ์กฐํšŒ - ``` +| ๋ฉ”์„œ๋“œ | ๊ฒฝ๋กœ | ์„ค๋ช… | +|--------|------|------| +| GET | `/api/travel/regions` | ์ง€์—ญ GeoJSON | +| GET | `/api/travel/photos` | ์‚ฌ์ง„ ๋ชฉ๋ก (region, page, size) | +| POST | `/api/travel/reload` | ์บ์‹œ ์ดˆ๊ธฐํ™” | + +- ์ธ๋„ค์ผ: `/media/travel/.thumb/{album}/{file}` (nginx ์ง์ ‘ ์„œ๋น™, 30์ผ ์บ์‹œ) +- ์›๋ณธ: `/media/travel/{album}/{file}` (nginx ์ง์ ‘ ์„œ๋น™, 7์ผ ์บ์‹œ) --- -### ๐ŸŸจ Travel Proxy (travel-proxy) +## ํ•ต์‹ฌ ๋กœ์ง -- **์—ญํ• **: ์—ฌํ–‰ ์‚ฌ์ง„ API, ์ง€์—ญ๋ณ„ ์‚ฌ์ง„ ๋งค์นญ, ์ธ๋„ค์ผ ์ž๋™ ์ƒ์„ฑ ๋ฐ ์บ์‹œ๋ฅผ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. -- **์„ค๊ณ„ ํฌ์ธํŠธ**: - - ์›๋ณธ ์‚ฌ์ง„์€ ์ฝ๊ธฐ ์ „์šฉ(RO)์œผ๋กœ ๋งˆ์šดํŠธํ•ฉ๋‹ˆ๋‹ค. - - ์ธ๋„ค์ผ์€ ์“ฐ๊ธฐ/์ฝ๊ธฐ(RW) ์ „์šฉ ์บ์‹œ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. - - ์‚ฌ์ง„ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์บ์‹œ๊ฐ€ ์ž๋™์œผ๋กœ ๋ฌดํšจํ™”๋ฉ๋‹ˆ๋‹ค. -- **๋ฐ์ดํ„ฐ ๊ตฌ์กฐ**: - ``` - /data/travel/ # ์›๋ณธ ์‚ฌ์ง„ (RO) - โ”œโ”€โ”€ 24.09.jeju/ - โ”œโ”€โ”€ 25.07.maldives/ - โ””โ”€โ”€ _meta/ - โ”œโ”€โ”€ region_map.json - โ””โ”€โ”€ regions.geojson +### ๋ชฌํ…Œ์นด๋ฅผ๋กœ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (lotto-backend) - /data/thumbs/ # ์ธ๋„ค์ผ ์บ์‹œ (RW) - โ”œโ”€โ”€ 24.09.jeju/ - โ””โ”€โ”€ 25.07.maldives/ - ``` -- **์ฃผ์š” ์—”๋“œํฌ์ธํŠธ**: - ```http - GET /api/travel/regions - GET /api/travel/photos?region=jeju - GET /media/travel/.thumb/{album}/{file} - ``` +``` +์—ญ๋Œ€ ๋‹น์ฒจ๋ฒˆํ˜ธ ๋ถ„์„ โ†’ ๋ฒˆํ˜ธ๋ณ„ ๊ฐ€์ค‘์น˜ ์‚ฐ์ถœ +โ†’ ๊ฐ€์ค‘ ํ™•๋ฅ  ์ƒ˜ํ”Œ๋ง์œผ๋กœ ํ›„๋ณด 20,000๊ฐœ ์ƒ์„ฑ +โ†’ 5๊ฐ€์ง€ ๊ธฐ๋ฒ•์œผ๋กœ ๊ฐ ์กฐํ•ฉ ์ ์ˆ˜ํ™” +โ†’ ์ƒ์œ„ 100๊ฐœ DB ์ €์žฅ โ†’ best_picks 20๊ฐœ ๊ต์ฒด +``` + +**5๊ฐ€์ง€ ์ฑ„์  ๊ธฐ๋ฒ•:** + +| ๊ธฐ๋ฒ• | ๊ฐ€์ค‘์น˜ | ๋‚ด์šฉ | +|------|--------|------| +| ๋นˆ๋„ Z-score | 25% | ๋ฒˆํ˜ธ ์ถœํ˜„ ๋นˆ๋„์˜ ํ‘œ์ค€ํŽธ์ฐจ | +| ์กฐํ•ฉ ์ง€๋ฌธ | 30% | ํ•ฉ๊ณ„ ์ •๊ทœ๋ถ„ํฌ + ํ™€์ง ๋น„์œจ + ๊ตฌ๊ฐ„๋ถ„ํฌ | +| ๊ฐญ ๋ถ„์„ | 20% | ๋งˆ์ง€๋ง‰ ์ถœํ˜„ ์ดํ›„ ๊ฒฝ๊ณผ ํšŒ์ฐจ | +| ๊ณต๋™ ์ถœํ˜„ | 15% | ๋ฒˆํ˜ธ ์Œ ๋™์‹œ ์ถœํ˜„ ๋นˆ๋„ | +| ๋‹ค์–‘์„ฑ | 10% | ์—ฐ์†๋ฒˆํ˜ธยท๋ฒ”์œ„ยท๊ตฌ๊ฐ„ ์ปค๋ฒ„๋ฆฌ์ง€ | + +**์Šค์ผ€์ค„:** ๋งค์ผ 0, 4, 8, 12, 16, 20์‹œ (ํ•˜๋ฃจ 6ํšŒ, ๊ฐ 5๋ถ„) + +### ์ด ์ž์‚ฐ ์Šค๋ƒ…์ƒท (stock-lab) + +``` +ํ‰์ผ 15:40 ์ž๋™ ์‹คํ–‰ โ†’ holidays.json์œผ๋กœ ๊ณตํœด์ผ ์Šคํ‚ต +โ†’ ํฌํŠธํด๋ฆฌ์˜ค ํ˜„์žฌ๊ฐ€ ์กฐํšŒ โ†’ total_eval +โ†’ ์˜ˆ์ˆ˜๊ธˆ ํ•ฉ๊ณ„ โ†’ total_cash +โ†’ asset_snapshots upsert (date UNIQUE, ๊ฐ™์€ ๋‚  ์ค‘๋ณต ์‹œ ๋ฎ์–ด์”€) +``` + +### ํ˜„์žฌ๊ฐ€ ์กฐํšŒ (stock-lab) + +- ๋„ค์ด๋ฒ„ ๋ชจ๋ฐ”์ผ API ์šฐ์„  (`m.stock.naver.com/api/stock/{ticker}/basic`) +- ์‹คํŒจ ์‹œ ๋„ค์ด๋ฒ„ ๊ธˆ์œต HTML ํŒŒ์‹ฑ ํด๋ฐฑ +- 3๋ถ„ TTL ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ + +### ์—ฌํ–‰ ์‚ฌ์ง„ ์ธ๋„ค์ผ (travel-proxy) + +- 480ร—480 ๋ฆฌ์‚ฌ์ด์ง• (Pillow), ํ™•์žฅ์ž ์œ ์ง€ (JPEG/PNG/WEBP) +- ์˜จ๋””๋งจ๋“œ ์ƒ์„ฑ ํ›„ `/data/thumbs/` ์˜๊ตฌ ์บ์‹œ +- ์›์ž์„ฑ ๋ณด์žฅ: tmp ํŒŒ์ผ ์ž‘์„ฑ ํ›„ rename --- -### ๐ŸŸฅ Deployer (webpage-deployer) +## ์ž๋™ ๋ฐฐํฌ -- **์—ญํ• **: Gitea Webhook์„ ์ˆ˜์‹ ํ•˜์—ฌ Git pull ๋ฐ Docker ์žฌ๊ธฐ๋™์„ ์ž๋™ํ™”ํ•ฉ๋‹ˆ๋‹ค. -- **ํ๋ฆ„**: `Gitea Push` โ†’ `Webhook` โ†’ `deployer` โ†’ `/scripts/deploy.sh` โ†’ `docker compose up -d --build` -- **๋ณด์•ˆ**: HMAC SHA256 ์„œ๋ช…(`X-Gitea-Signature`)์„ `WEBHOOK_SECRET` ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. -- **ํŠน์ง•**: - - Docker socket์„ ๋งˆ์šดํŠธํ•˜์—ฌ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. - - ๋กค๋ฐฑ์„ ์œ„ํ•ด `.releases/` ๋””๋ ‰ํ† ๋ฆฌ์— ์ž๋™ ๋ฐฑ์—…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. +``` +git push โ†’ Gitea โ†’ X-Gitea-Signature (HMAC SHA256) + โ†’ deployer:9000/webhook (์„œ๋ช… ๊ฒ€์ฆ, compare_digest ์‚ฌ์šฉ) + โ†’ BackgroundTask: scripts/deploy.sh (10๋ถ„ ํƒ€์ž„์•„์›ƒ) + 1. git pull + 2. .releases/{timestamp}/ ๋ฐฑ์—… + 3. rsync (repo โ†’ runtime) + 4. docker compose up -d --build + 5. chown PUID:PGID +``` + +> ํ”„๋ก ํŠธ์—”๋“œ๋Š” **์ž๋™ ๋ฐฐํฌ ์•ˆ ๋จ** โ€” ๋กœ์ปฌ ๋นŒ๋“œ ํ›„ NAS์— ์ˆ˜๋™ ์—…๋กœ๋“œ --- -## ๐Ÿ” ๋ฐฐํฌ ํ”Œ๋กœ์šฐ ์š”์•ฝ +## ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค -- **Backend / Travel / Infra ๋ณ€๊ฒฝ**: `git push`๋ฅผ ํ†ตํ•ด Gitea๋กœ ํ‘ธ์‹œํ•˜๋ฉด Webhook์ด ํŠธ๋ฆฌ๊ฑฐ๋˜์–ด ์ž๋™์œผ๋กœ ๋ฐฐํฌ๋ฉ๋‹ˆ๋‹ค. -- **Frontend ๋ณ€๊ฒฝ**: ๋กœ์ปฌ์—์„œ ๋นŒ๋“œ ํ›„, ์ƒ์„ฑ๋œ ์ •์  ํŒŒ์ผ๋งŒ NAS๋กœ ์—…๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค. +### lotto.db (`/app/data/lotto.db`) + +| ํ…Œ์ด๋ธ” | ์„ค๋ช… | +|--------|------| +| `draws` | ๋กœ๋˜ ๋‹น์ฒจ๋ฒˆํ˜ธ | +| `recommendations` | ์ถ”์ฒœ ์ด๋ ฅ (์ฆ๊ฒจ์ฐพ๊ธฐยทํƒœ๊ทธยท์ฑ„์  ํฌํ•จ) | +| `simulation_runs` | ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹คํ–‰ ๊ธฐ๋ก | +| `simulation_candidates` | ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ํ›„๋ณด (์ ์ˆ˜ 5์ข…) | +| `best_picks` | ํ˜„์žฌ ํ™œ์„ฑ ์ตœ์  ๋ฒˆํ˜ธ 20๊ฐœ (is_active ํ”Œ๋ž˜๊ทธ) | +| `todos` | ํˆฌ๋‘๋ฆฌ์ŠคํŠธ (UUID PK) | +| `blog_posts` | ๋ธ”๋กœ๊ทธ ๊ธ€ (tags: JSON ๋ฐฐ์—ด) | + +### stock.db (`/app/data/stock.db`) + +| ํ…Œ์ด๋ธ” | ์„ค๋ช… | +|--------|------| +| `articles` | ๋‰ด์Šค ๊ธฐ์‚ฌ (hash UNIQUE, category: domestic\|overseas) | +| `portfolio` | ๋ณด์œ  ์ข…๋ชฉ (broker, ticker, quantity, avg_price) | +| `broker_cash` | ์ฆ๊ถŒ์‚ฌ๋ณ„ ์˜ˆ์ˆ˜๊ธˆ (broker UNIQUE) | +| `asset_snapshots` | ์ผ๋ณ„ ์ด ์ž์‚ฐ ์Šค๋ƒ…์ƒท (date UNIQUE) | --- -## ๐Ÿงช ์šด์˜ ์ฒดํฌ ํฌ์ธํŠธ +## ํ™˜๊ฒฝ๋ณ€์ˆ˜ -- `/health`: Backend ์„œ๋น„์Šค ์ƒํƒœ ํ™•์ธ -- `/api/travel/photos`: ์‘๋‹ต ์†๋„ ํ™•์ธ -- `/media/travel/.thumb`: ์ธ๋„ค์ผ ์ƒ์„ฑ ์—ฌ๋ถ€ ํ™•์ธ -- `deployer` ์ปจํ…Œ์ด๋„ˆ ๋กœ๊ทธ ํ™•์ธ +```env +# ๊ฒฝ๋กœ ์„ค์ • +RUNTIME_PATH=. +REPO_PATH=. +FRONTEND_PATH=./frontend/dist +PHOTO_PATH=./mock_data/photos + +# NAS ํŒŒ์ผ ๊ถŒํ•œ +PUID=1000 +PGID=1000 + +# ์™ธ๋ถ€ ์„œ๋น„์Šค +WINDOWS_AI_SERVER_URL=http://192.168.45.59:8000 +WEBHOOK_SECRET=your_secret_here +``` --- -## ๐Ÿ“ TODO +## ์ธํ”„๋ผ -### ๐Ÿ”ฅ ๋กœ๋˜ ์„œ๋น„์Šค ๊ณ ๋„ํ™” - -- [ ] ์ถ”์ฒœ ๊ฒฐ๊ณผ ํ†ต๊ณ„ ์‹œ๊ฐํ™” (๋ถ„ํฌ, ํ•ฉ๊ณ„, ํ™€์ง) -- [ ] ์ถ”์ฒœ ํžˆ์Šคํ† ๋ฆฌ ํ•„ํ„ฐ ๋ฐ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ -- [ ] ์ถ”์ฒœ ๊ฒฐ๊ณผ ์ฆ๊ฒจ์ฐพ๊ธฐ UI -- [ ] ํšŒ์ฐจ ๋Œ€๋น„ ์ถ”์ฒœ ์„ฑ๋Šฅ ๋ถ„์„ - -### ๐Ÿ—บ๏ธ ์—ฌํ–‰ ์ง€๋„ UI - -- [ ] ์ง€๋„ ์˜์—ญ ํด๋ฆญ ์‹œ ํ•ด๋‹น ์ง€์—ญ ์‚ฌ์ง„ ๋กœ๋”ฉ -- [ ] ์‚ฌ์ง„ ์ง€์—ฐ ๋กœ๋”ฉ (Lazy Load) -- [ ] ์•จ๋ฒ” ๋ฐ ์—ฐ๋„๋ณ„ ํ•„ํ„ฐ ๊ธฐ๋Šฅ -- [ ] ๋ชจ๋ฐ”์ผ UX ๊ฐœ์„  - -### โš™๏ธ ์šด์˜/์ธํ”„๋ผ - -- [ ] Docker ์ด๋ฏธ์ง€ ๋ฒ„์ „ ํƒœ๊น… ์ž๋™ํ™” -- [ ] ๋ฐฐํฌ ์‹คํŒจ ์‹œ ์ž๋™ ๋กค๋ฐฑ ๊ธฐ๋Šฅ -- [ ] Health check ๊ธฐ๋ฐ˜ ๋ฐฐํฌ ์„ฑ๊ณต ํŒ๋‹จ ๋กœ์ง -- [ ] ๋กœ๊ทธ ์ˆ˜์ง‘ ๋ฐ ๊ด€๋ฆฌ ์ฒด๊ณ„ ๊ฐœ์„  +| ํ•ญ๋ชฉ | ๊ฐ’ | +|------|----| +| ์žฅ๋น„ | Synology NAS (Intel Celeron J4025, 18GB RAM) | +| Docker | Synology Container Manager | +| Git ์„œ๋ฒ„ | Gitea (NAS ๋‚ด๋ถ€ self-hosted) | +| AI ์„œ๋ฒ„ | Windows PC (192.168.45.59:8000) โ€” RTX 3070 Ti + Ollama | +| Python | 3.12 (`slim` / `alpine` ๊ธฐ๋ฐ˜ ์ด๋ฏธ์ง€) | +| DB | SQLite (๋ณผ๋ฅจ ๋งˆ์šดํŠธ๋กœ ์˜์† ์ €์žฅ) | --- -## โœจ ์ฒ ํ•™ +## ์ฃผ์˜์‚ฌํ•ญ -> โ€œNAS๋Š” ์„œ๋ฒ„๊ฐ€ ์•„๋‹ˆ๋ผ ์ง‘์ด๋‹ค.โ€ -> -> ๊ทธ๋ž˜์„œ ์•ˆ์ „ํ•˜๊ณ , ๋‹จ์ˆœํ•˜๋ฉฐ, ๋ณต๊ตฌ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ๋ฅผ ์ตœ์šฐ์„ ์œผ๋กœ ํ•ฉ๋‹ˆ๋‹ค. - ---- - -## Makefile ์„ค์ • ์‚ฌ์šฉ ์˜ˆ์‹œ - -- **๋ฐฐํฌ**: `make deploy` -- **๋ฐฑ์—”๋“œ ๋กœ๊ทธ**: `make logs S=backend` -- **์ „์ฒด ๋กœ๊ทธ**: `make logs` -- **์ƒํƒœ**: `make status` +- **`.env` ํŒŒ์ผ** โ€” ์ ˆ๋Œ€ ์ปค๋ฐ‹ ๊ธˆ์ง€. `.env.example`๋งŒ ๋ ˆํฌ์— ํฌํ•จ +- **Nginx trailing slash** โ€” `/api/portfolio`๋Š” ๋‘ location ๋ธ”๋ก์œผ๋กœ ์ฒ˜๋ฆฌ (trailing slash ์œ ๋ฌด ๋ชจ๋‘ ๋งค์นญ) +- **๋ผ์šฐํŠธ ์ˆœ์„œ** โ€” `/api/todos/done`์€ `/api/todos/{id}` ๋ณด๋‹ค ๋จผ์ € ๋“ฑ๋ก ํ•„์ˆ˜ +- **์บ์‹œ ์ „๋žต** โ€” `index.html`: no-store / `assets/`: 1๋…„ immutable +- **PUID/PGID** โ€” travel-proxy๋Š” NAS ํŒŒ์ผ ๊ถŒํ•œ์„ ์œ„ํ•ด ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ฃผ์ž… ํ•„์ˆ˜ +- **๊ณตํœด์ผ ๋ชฉ๋ก** โ€” `stock-lab/app/holidays.json` ๋งค๋…„ ์ˆ˜๋™ ๊ฐฑ์‹  ํ•„์š” (KRX ๊ธฐ์ค€) +- **Windows AI ์„œ๋ฒ„** โ€” IP 192.168.45.59 (๊ณต์œ ๊ธฐ DHCP ๊ณ ์ • ์˜ˆ์•ฝ)