# 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 고정 예약)