9e7efc3f12e26955c55ba6b2a9e3d048b8ad81d0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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
빠른 시작 (로컬 개발)
# 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) |
환경변수
# 경로 설정
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 고정 예약)
Description
Languages
Python
98.7%
Shell
0.8%
Dockerfile
0.4%
Jinja
0.1%