335 lines
12 KiB
Markdown
335 lines
12 KiB
Markdown
# web-backend
|
||
|
||
Synology NAS 기반 개인 웹 플랫폼 백엔드 모노레포.
|
||
로또 분석, 주식 포트폴리오, 여행 앨범, 블로그, 투두리스트를 하나의 서비스로 운영한다.
|
||
|
||
---
|
||
|
||
## 서비스 구성
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 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
|
||
│
|
||
├── stock-lab/ # stock-lab 서비스 (Python/FastAPI)
|
||
│ ├── app/
|
||
│ │ ├── main.py # 라우터, 스케줄러
|
||
│ │ ├── db.py # SQLite CRUD (4개 테이블)
|
||
│ │ ├── scraper.py # 네이버 금융 뉴스 크롤링
|
||
│ │ ├── price_fetcher.py # 현재가 조회 (3분 캐시)
|
||
│ │ └── holidays.json # 한국 주식시장 휴장일
|
||
│ └── Dockerfile
|
||
│
|
||
├── 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
|
||
```
|
||
|
||
---
|
||
|
||
## 빠른 시작 (로컬 개발)
|
||
|
||
```bash
|
||
# 1. 환경변수 설정
|
||
cp .env.example .env
|
||
|
||
# 2. 컨테이너 실행 (.env 기본값으로 즉시 실행 가능)
|
||
docker compose up -d
|
||
|
||
# 3. 확인
|
||
curl http://localhost:18000/health
|
||
curl http://localhost:18500/health
|
||
```
|
||
|
||
| 서비스 | 로컬 URL |
|
||
|--------|----------|
|
||
| Frontend + API | http://localhost:8080 |
|
||
| lotto-backend | http://localhost:18000 |
|
||
| stock-lab | http://localhost:18500 |
|
||
| travel-proxy | http://localhost:19000 |
|
||
|
||
---
|
||
|
||
## API 목록
|
||
|
||
### 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 }`
|
||
|
||
---
|
||
|
||
### stock-lab (`/api/stock/`, `/api/trade/`, `/api/portfolio`)
|
||
|
||
#### 뉴스 & 지표
|
||
|
||
| 메서드 | 경로 | 설명 |
|
||
|--------|------|------|
|
||
| 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: 전체) |
|
||
|
||
---
|
||
|
||
### travel-proxy (`/api/travel/`)
|
||
|
||
| 메서드 | 경로 | 설명 |
|
||
|--------|------|------|
|
||
| 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일 캐시)
|
||
|
||
---
|
||
|
||
## 핵심 로직
|
||
|
||
### 몬테카를로 시뮬레이션 (lotto-backend)
|
||
|
||
```
|
||
역대 당첨번호 분석 → 번호별 가중치 산출
|
||
→ 가중 확률 샘플링으로 후보 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
|
||
|
||
---
|
||
|
||
## 자동 배포
|
||
|
||
```
|
||
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에 수동 업로드
|
||
|
||
---
|
||
|
||
## 데이터베이스
|
||
|
||
### 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) |
|
||
|
||
---
|
||
|
||
## 환경변수
|
||
|
||
```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
|
||
```
|
||
|
||
---
|
||
|
||
## 인프라
|
||
|
||
| 항목 | 값 |
|
||
|------|----|
|
||
| 장비 | 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 (볼륨 마운트로 영속 저장) |
|
||
|
||
---
|
||
|
||
## 주의사항
|
||
|
||
- **`.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 고정 예약)
|