# CLAUDE.md — web-backend 프로젝트 가이드 > Claude Code가 이 프로젝트를 작업할 때 참조하는 설정 및 구조 문서. --- ## 1. 프로젝트 개요 Synology NAS 기반의 개인 웹 플랫폼 백엔드 모노레포. - **서비스**: lotto-lab, stock-lab, travel-album, music-lab, deployer - **프론트엔드**: 별도 레포 (React + Vite SPA), 빌드 산출물만 NAS에 배포 - **인프라**: Docker Compose + Nginx(리버스 프록시) + Gitea Webhook 자동 배포 --- ## 2. NAS 환경 | 항목 | 값 | |------|----| | 장비 | Synology NAS | | CPU | Intel Celeron J4025 (2 Core, 2.0 GHz) | | 메모리 | 18 GB | | Docker | Synology Container Manager | | Git 서버 | Gitea (self-hosted, NAS 내부) | | AI 서버 | Windows PC (192.168.45.59:8000) — NVIDIA 3070 Ti + Ollama | --- ## 3. NAS 디렉토리 구조 ``` /volume1 ├── docker/webpage/ # 운영 런타임 (Docker Compose 실행 위치) │ ├── backend/ # lotto-backend 소스 (rsync 동기화) │ ├── stock-lab/ # stock-lab 소스 (rsync 동기화) │ ├── travel-proxy/ # travel-proxy 소스 (rsync 동기화) │ ├── deployer/ # deployer 소스 (rsync 동기화) │ ├── nginx/default.conf # Nginx 설정 │ ├── scripts/deploy.sh # Webhook 트리거 배포 스크립트 │ ├── docker-compose.yml │ ├── .env # 운영 환경변수 │ ├── data/lotto.db # SQLite DB │ └── data/music/ # 생성된 오디오 파일 (music-lab) │ ├── workspace/web-page-backend/ # Git 레포 클론 위치 (REPO_PATH) │ └── web/images/webPage/travel/ # 원본 여행 사진 (RO 마운트) ``` --- ## 4. Docker 서비스 & 포트 | 컨테이너 | 포트 | 역할 | |---------|------|------| | `lotto-backend` | 18000 | 로또 데이터 수집·분석·추천 API | | `stock-lab` | 18500 | 주식 뉴스·AI 분석·KIS API 연동 | | `music-lab` | 18600 | AI 음악 생성·라이브러리 관리 API | | `travel-proxy` | 19000 | 여행 사진 API + 썸네일 생성 | | `lotto-frontend` (nginx) | 8080 | 정적 SPA 서빙 + API 리버스 프록시 | | `webpage-deployer` | 19010 | Gitea Webhook 수신 → 자동 배포 | --- ## 5. Nginx 라우팅 규칙 | 경로 | 프록시 대상 | 비고 | |------|------------|------| | `/api/` | `lotto-backend:8000` | lotto API (기본) | | `/api/travel/` | `travel-proxy:8000` | travel API | | `/api/stock/` | `stock-lab:8000` | stock API | | `/api/trade/` | `stock-lab:8000` | KIS 실계좌 API | | `/api/portfolio` | `stock-lab:8000` | trailing slash 유무 모두 매칭 | | `/api/music/` | `music-lab:8000` | AI 음악 생성·라이브러리 API | | `/webhook`, `/webhook/` | `deployer:9000` | Gitea Webhook | | `/media/music/` | `/data/music/` (파일 직접 서빙) | 생성된 오디오 파일 | | `/media/travel/.thumb/` | `/data/thumbs/` (파일 직접 서빙) | 썸네일 캐시 | | `/media/travel/` | `/data/travel/` (파일 직접 서빙) | 원본 사진 | | `/assets/` | 정적 파일 (장기 캐시) | Vite 해시 파일 | | `/` | SPA fallback (`try_files → index.html`) | | --- ## 6. 기술 스택 | 레이어 | 기술 | |--------|------| | Backend 언어 | Python 3.12 | | API 프레임워크 | FastAPI | | DB | SQLite (`/app/data/*.db`) | | 스케줄러 | APScheduler | | 컨테이너 | Docker (`python:3.12-slim` 기반) | | AI 연동 | Ollama (Llama 3.1) — Windows PC (192.168.45.59) | | 주식 API | KIS (한국투자증권) Open API | --- ## 7. 자동 배포 흐름 ``` 개발자 git push → Gitea → Webhook (HMAC SHA256 검증) → deployer 컨테이너 → /scripts/deploy.sh → rsync(REPO→RUNTIME) → docker compose up -d --build ``` - **배포 스크립트 위치**: `scripts/deploy-nas.sh` (레포) / `scripts/deploy.sh` (런타임) - **환경변수 파일**: `.env` (RUNTIME_PATH, REPO_PATH, PHOTO_PATH, PUID, PGID 등) - **백업**: `.releases/` 디렉토리에 자동 백업 --- ## 8. 로컬 개발 환경 ```bash # .env 기본값으로 즉시 실행 가능 (RUNTIME_PATH=., PHOTO_PATH=./mock_data/photos) docker compose up -d ``` | 서비스 | 로컬 URL | |--------|----------| | Frontend + API | http://localhost:8080 | | Lotto Backend | http://localhost:18000 | | Travel API | http://localhost:19000 | | Stock Lab | http://localhost:18500 | --- ## 9. 서비스별 핵심 정보 ### lotto-lab (backend/) - DB: `/app/data/lotto.db` - 데이터 소스: `smok95.github.io/lotto/results/` - 파일 구조: `main.py`, `db.py`, `recommender.py`, `collector.py`, `checker.py`, `generator.py`, `analyzer.py`, `utils.py` **lotto.db 테이블** | 테이블 | 설명 | |--------|------| | `draws` | 로또 당첨번호 | | `recommendations` | 추천 이력 (즐겨찾기·태그·채점 포함) | | `simulation_runs` | 시뮬레이션 실행 기록 | | `simulation_candidates` | 시뮬레이션 후보 (점수 5종) | | `best_picks` | 현재 활성 최적 번호 20개 (`is_active` 플래그로 교체) | | `todos` | 투두리스트 (UUID PK) | | `blog_posts` | 블로그 글 (tags: JSON 배열) | **스케줄러 job** - 09:10 / 21:10 매일 — 당첨번호 동기화 + 채점 (`sync_latest` → `check_results_for_draw`) - 00:05, 04:05, 08:05, 12:05, 16:05, 20:05 — 몬테카를로 시뮬레이션 (20,000후보 → 상위100 → best_picks 20개 교체) **lotto-lab 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/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}` | 투두 개별 삭제 | | GET | `/api/blog/posts` | 블로그 글 목록 (`{"posts": [...]}`, date DESC) | | POST | `/api/blog/posts` | 블로그 글 생성 (date 미입력 시 오늘) | | PUT | `/api/blog/posts/{id}` | 블로그 글 수정 | | DELETE | `/api/blog/posts/{id}` | 블로그 글 삭제 | ### stock-lab (stock-lab/) - Windows AI 서버 연동: `WINDOWS_AI_SERVER_URL=http://192.168.45.59:8000` - KIS API 연동으로 실계좌 잔고·거래 조회 - 뉴스 스크래핑: 네이버 증권 + 해외 사이트 - DB: `/app/data/stock.db` (articles, portfolio, broker_cash, asset_snapshots, sell_history 테이블) - 파일 구조: `main.py`, `db.py`, `scraper.py`, `price_fetcher.py`, `holidays.json` **stock-lab API 목록** | 메서드 | 경로 | 설명 | |--------|------|------| | GET | `/api/stock/news` | 뉴스 조회 (`limit`, `category` 파라미터) | | GET | `/api/stock/indices` | 주요 지표 실시간 조회 | | POST | `/api/stock/scrap` | 수동 뉴스 스크랩 트리거 | | GET | `/api/trade/balance` | 실계좌 잔고 조회 (Windows AI 서버 프록시) | | POST | `/api/trade/order` | 주식 주문 (Windows AI 서버 프록시) | | 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`: 전체, `days=N`: 최근 N건) | | GET | `/api/portfolio/sell-history` | 매도 내역 조회 (`broker`, `days` 필터 선택) | | POST | `/api/portfolio/sell-history` | 매도 기록 저장 (id 포함 레코드 반환) | | PUT | `/api/portfolio/sell-history/{id}` | 매도 기록 수정 (수정된 레코드 반환) | | DELETE | `/api/portfolio/sell-history/{id}` | 매도 기록 삭제 | **매도 히스토리 (`sell_history`)** - 독립 테이블 — `portfolio` 테이블과 별개로 관리 - `sold_at`: UTC ISO8601 형식 (`new Date().toISOString()`) - `realized_profit` / `realized_rate`: 프론트 계산값 저장 (백엔드 재계산 무방) - 응답 정렬: `sold_at DESC` (최신순) **총 자산 스냅샷 (`asset_snapshots`)** - 평일 15:40 APScheduler 자동 실행 (`save_daily_snapshot`) - 공휴일 판별: `holidays.json` (매년 수동 갱신, KRX 기준) → `is_market_open()` 함수 - 같은 날 중복 저장 시 upsert (date UNIQUE 제약) - 수동 저장: `POST /api/portfolio/snapshot` - 이력 조회: `GET /api/portfolio/snapshot/history?days=30` (ASC 정렬, 차트용) **스케줄러 job** - 08:00 매일 — 뉴스 스크랩 (`run_scraping_job`) - 15:40 평일 — 총 자산 스냅샷 저장 (`save_daily_snapshot`) ### music-lab (music-lab/) - 듀얼 프로바이더 음악 생성 서비스 (Suno API + 로컬 MusicGen) - 생성된 오디오 파일: `/app/data/music/` (Nginx가 `/media/music/`로 직접 서빙) - DB: `/app/data/music.db` (music_tasks, music_library 테이블) - 파일 구조: `main.py`, `db.py`, `suno_provider.py`, `local_provider.py` - 생성 흐름: POST generate (provider 지정) → task_id 반환 → BackgroundTask → 파일 저장 → 라이브러리 자동 등록 **Provider 구조** - `suno`: Suno REST API (`apicast.suno.ai/v1`) — 보컬·가사·인스트루멘탈 지원 - `local`: Windows AI 서버 (MusicGen) — 인스트루멘탈 전용 **music-lab API 목록** | 메서드 | 경로 | 설명 | |--------|------|------| | GET | `/api/music/providers` | 사용 가능한 프로바이더 목록 | | POST | `/api/music/generate` | 음악 생성 시작 (provider, lyrics, instrumental 지원) | | GET | `/api/music/status/{task_id}` | 생성 상태 폴링 (queued→processing→succeeded/failed) | | POST | `/api/music/lyrics` | Suno AI 가사 생성 (곡 생성 전 미리보기용) | | GET | `/api/music/library` | 라이브러리 전체 조회 | | POST | `/api/music/library` | 트랙 수동 추가 (201) | | DELETE | `/api/music/library/{id}` | 트랙 삭제 (로컬 파일 포함) | **환경변수** - `SUNO_API_KEY`: Suno API 키 (미설정 시 Suno provider 비활성화) - `MUSIC_AI_SERVER_URL`: 로컬 MusicGen 서버 URL (미설정 시 local provider 비활성화) - `MUSIC_MEDIA_BASE`: 오디오 파일 공개 URL prefix (기본 `/media/music`) - `MUSIC_DATA_PATH`: NAS 오디오 파일 저장 경로 (기본 `./data/music`) **music_library 테이블 (확장 컬럼)** - `provider`: `suno` | `local` — 생성에 사용된 프로바이더 - `lyrics`: Suno 생성 가사 텍스트 - `image_url`: Suno 생성 커버 이미지 URL - `suno_id`: Suno 곡 ID (CDN 참조용) **Suno 생성 특이사항** - 1회 생성 시 2개 변형(variation) 반환 → 둘 다 라이브러리에 저장 - CDN URL(`cdn1.suno.ai`)은 임시 → 반드시 로컬 다운로드 필요 - 가사 섹션 태그: `[Verse]`, `[Chorus]`, `[Bridge]`, `[Instrumental]` 등 ### travel-proxy (travel-proxy/) - 원본 사진: `/data/travel/` (RO) - 썸네일 캐시: `/data/thumbs/` (RW) - 메타: `/data/travel/_meta/region_map.json`, `regions.geojson` - 썸네일: 480×480 리사이징 (Pillow), 온디맨드 생성 후 영구 캐시 - 메모리 캐시: TTL 300초 (앨범 스캔 결과) **travel-proxy API 목록** | 메서드 | 경로 | 설명 | |--------|------|------| | GET | `/api/travel/regions` | 지역 GeoJSON | | GET | `/api/travel/photos` | 사진 목록 (region, page=1, size=20) | | POST | `/api/travel/reload` | 메모리 캐시 초기화 | ### deployer (deployer/) - Webhook 검증: `X-Gitea-Signature` (HMAC SHA256, `compare_digest` 사용) - `WEBHOOK_SECRET` 환경변수로 시크릿 관리 - Webhook 수신 즉시 `{"ok": True}` 응답 후 BackgroundTask로 배포 실행 - 배포 타임아웃: 10분 (`scripts/deploy.sh`) --- ## 10. 주의사항 - **Nginx trailing slash**: `/api/portfolio`는 trailing slash 없이도 매칭되도록 두 location 블록으로 처리 - **라우트 순서**: `DELETE /api/todos/done`은 `DELETE /api/todos/{id}` 보다 **반드시 먼저** 등록 (FastAPI prefix 매칭 순서) - **PUID/PGID**: travel-proxy는 NAS 파일 권한을 위해 PUID/PGID를 환경변수로 주입 - **캐시 전략**: `index.html`은 `no-store`, `assets/`는 1년 장기 캐시(immutable) - **Frontend 배포**: git push로 자동 배포되지 않음. 로컬 빌드 후 NAS에 수동 업로드 - **.env 파일**: 절대 커밋 금지. `.env.example`만 레포에 포함 - **공휴일 목록**: `stock-lab/app/holidays.json` 매년 수동 갱신 필요 (KRX 기준) - **Windows AI 서버 IP**: `192.168.45.59` — 공유기 DHCP 고정 예약으로 고정. Tailscale은 Synology에서 TCP 불가(userspace 모드)라 로컬 IP 사용 - **현재가 조회**: 네이버 모바일 API → HTML 파싱 폴백, 3분 TTL 캐시 (`price_fetcher.py`) - **시뮬레이션 교체 방식**: `best_picks`는 교체형 — 새 시뮬레이션 실행 시 `is_active=0`으로 비활성화 후 신규 입력