Files
web-page-backend/README.md
2026-03-11 08:30:47 +09:00

335 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 고정 예약)