# web-backend Synology NAS 기반 개인 웹 플랫폼 백엔드 모노레포. 로또 분석, 주식 포트폴리오, AI 음악 생성, 블로그 마케팅, 부동산 청약, AI 에이전트 오피스, 여행 앨범을 하나의 Docker Compose 스택으로 운영한다. --- ## 서비스 구성 ``` ┌──────────────────────────────────────────────────────────────────────┐ │ lotto-frontend (Nginx:8080) │ │ ├── 정적 SPA 서빙 (React + Vite) │ │ └── API 리버스 프록시 │ │ ├── /api/ → lotto-backend:8000 (로또·블로그·투두)│ │ ├── /api/stock/, /trade/ → stock:8000 │ │ ├── /api/portfolio → stock:8000 │ │ ├── /api/music/ → music-lab:8000 │ │ ├── /api/blog-marketing/ → blog-lab:8000 │ │ ├── /api/realestate/ → realestate-lab:8000 │ │ ├── /api/agent-office/ → agent-office:8000 (+ WebSocket) │ │ ├── /api/travel/ → travel-proxy:8000 │ │ ├── /media/music/… (nginx 직접 서빙, 생성 오디오) │ │ ├── /media/travel/… (nginx 직접 서빙, 사진/썸네일) │ │ └── /webhook → deployer:9000 │ └──────────────────────────────────────────────────────────────────────┘ ``` | 컨테이너 | 포트 | 역할 | |---------|------|------| | `lotto-backend` | 18000 | 로또 데이터 수집·분석·추천 + 블로그·투두 API | | `stock` | 18500 | 주식 뉴스·AI 요약·KIS 실계좌·포트폴리오·자산 추적 | | `music-lab` | 18600 | AI 음악 생성 (Suno + 로컬 MusicGen 듀얼 프로바이더) | | `blog-lab` | 18700 | 블로그 마케팅 수익화 (키워드→글 생성→리뷰→발행) | | `realestate-lab` | 18800 | 청약 공고 자동 수집·프로필 매칭 | | `agent-office` | 18900 | AI 에이전트 가상 오피스 (WebSocket + 텔레그램 봇) | | `travel-proxy` | 19000 | 여행 사진 API + 온디맨드 썸네일 | | `lotto-frontend` | 8080 | SPA 서빙 + 리버스 프록시 | | `webpage-deployer` | 19010 | Gitea Webhook → 자동 배포 | --- ## 디렉토리 구조 ``` web-backend/ ├── backend/ # lotto-backend (로또·블로그·투두) ├── stock/ # 주식·포트폴리오 ├── music-lab/ # AI 음악 생성 ├── blog-lab/ # 블로그 마케팅 파이프라인 ├── realestate-lab/ # 청약 자동 수집·매칭 ├── agent-office/ # AI 에이전트 오피스 (WS + 텔레그램) ├── travel-proxy/ # 여행 사진 + 썸네일 ├── deployer/ # Gitea Webhook 수신 → 자동 배포 ├── nginx/default.conf # 리버스 프록시 + SPA + 캐시 ├── scripts/ # deploy.sh, deploy-nas.sh, healthcheck.sh ├── docker-compose.yml ├── .env.example └── CLAUDE.md # Claude Code 작업용 상세 컨텍스트 ``` --- ## 빠른 시작 (로컬 개발) ```bash cp .env.example .env docker compose up -d curl http://localhost:18000/health curl http://localhost:18500/health ``` | 서비스 | 로컬 URL | |--------|----------| | Frontend + API | http://localhost:8080 | | lotto-backend | http://localhost:18000 | | stock | http://localhost:18500 | | music-lab | http://localhost:18600 | | blog-lab | http://localhost:18700 | | realestate-lab | http://localhost:18800 | | agent-office | http://localhost:18900 | | travel-proxy | http://localhost:19000 | --- ## 서비스별 기능 ### 1. lotto-backend (`/api/`) 로또 당첨번호 수집·통계 분석·몬테카를로 시뮬레이션 기반 추천 + 투두·블로그 CRUD. - **로또**: 당첨번호 조회, 5종 통계 분석, 시뮬레이션 최적 번호(`best_picks` 20쌍), 통계/히트맵/스마트/배치 추천, 전략 가중치(EMA+Softmax), 구매 이력 관리 - **추천 이력**: 즐겨찾기·태그·메모 관리 - **투두리스트**: UUID PK, 상태(todo/in_progress/done) - **블로그**: 일기형 포스트 (tags JSON 배열, date DESC) **스케줄러** - 09:10 / 21:10 — 당첨번호 동기화 + 추천 채점 - 00:05, 04:05, 08:05, 12:05, 16:05, 20:05 — 몬테카를로 시뮬레이션 (후보 20,000 → 상위 100 → best_picks 20쌍 교체) ### 2. stock (`/api/stock/`, `/api/trade/`, `/api/portfolio`) 주식 뉴스 스크래핑 + LLM 요약 + KIS 실계좌 연동 + 포트폴리오·자산 스냅샷. - **뉴스**: 네이버 증권 + 해외 사이트 크롤링, LLM 기반 한국어 요약 - **실계좌**: Windows AI 서버(192.168.45.59:8000) 프록시 → KIS Open API (잔고/주문) - **포트폴리오**: 종목·예수금·매도 히스토리 관리, 현재가 자동 조회 - **자산 스냅샷**: 평일 15:40 자동 저장 (KRX 공휴일 판별, `holidays.json` 매년 갱신) **LLM provider 전환** — `LLM_PROVIDER` 환경변수 - `claude` (기본): Anthropic Messages API (`claude-haiku-4-5`) - `ollama`: Windows AI 서버 Ollama (`qwen3:14b`) **현재가 조회**: 네이버 모바일 API → HTML 파싱 폴백, 3분 TTL 메모리 캐시 ### 3. music-lab (`/api/music/`) 듀얼 프로바이더 AI 음악 생성. - **Suno** (`suno`): REST API 연동, 보컬·가사·인스트루멘탈. 1회 요청 시 2개 variation 생성, 곡 연장, 보컬 분리, WAV 변환, 12스템 분리, 뮤직비디오, AI Cover 등 풀 스위트 지원 - **로컬 MusicGen** (`local`): Windows AI PC(RTX 5070 Ti, 16GB VRAM) 인스트루멘탈 전용 - **라이브러리**: 생성 파일은 `/app/data/music/`에 저장되고 Nginx가 `/media/music/`으로 직접 서빙 - **가사 도구**: 저장·편집·타임스탬프 기반 가라오케 동기 ### 4. blog-lab (`/api/blog-marketing/`) 블로그 마케팅 수익화 4단계 파이프라인 (`draft → marketed → reviewed → published`). ``` 리서치(Naver Search + 상위 블로그 본문 크롤링) → 작가(AI 초안 생성) → 마케터(전환율 강화 + 브랜드 링크 삽입) → 평가자(6기준×10점, 42/60 통과 시 published) ``` - **AI 엔진**: Claude API (`claude-sonnet-4-20250514`) - **키워드 분석**: 네이버 검색(블로그+쇼핑) API + 경쟁도/기회 점수 - **수익 추적**: 포스트별 월간 클릭/구매/수익 기록 - **프롬프트 템플릿**: DB에 저장 → 코드 배포 없이 수정 가능 ### 5. realestate-lab (`/api/realestate/`) 공공데이터포털 청약홈 API 연동 + 프로필 기반 자동 매칭. - **공고 수집**: 09:00 매일 자동 (`DATA_GO_KR_API_KEY` 필요) - **상태 갱신 + 재매칭**: 00:00 매일 자동 - **프로필 매칭**: 지역·주택형·소득·부양가족 등으로 점수화, 신규 매칭 알림 - **대시보드**: 진행 중 공고수, 신규 매칭수, 다가오는 일정 요약 ### 6. agent-office (`/api/agent-office/`) AI 에이전트 가상 오피스 — 2D 픽셀아트 사무실에서 4명의 에이전트가 실제 작업을 수행한다. - **아키텍처**: stock / music-lab / blog-lab / realestate-lab 기존 API를 서비스 프록시로 호출 (직접 DB 접근 없음) - **FSM 상태**: `idle → working → waiting(승인 대기) → reporting → break` - **실시간 동기화**: WebSocket `/api/agent-office/ws` (init, agent_state, task_complete, command_result) - **텔레그램 연동**: 양방향 알림 + 인라인 키보드 승인 - 봇이 작업 결과를 텔레그램으로 푸시, 명령은 텔레그램에서 바로 에이전트에 전달 - Webhook 검증 후 `chat.id` 기준 라우팅 #### 에이전트 구성 | 에이전트 | 스케줄 | 승인 | 주요 기능 | |---------|--------|-----|----------| | 📈 **주식 트레이더** (`stock`) | 08:00 매일 | — | 뉴스 요약 (LLM) → 텔레그램 아침 브리핑, 종목 알람 등록 | | 🎵 **음악 프로듀서** (`music`) | 수동 트리거 | ✅ 작곡 | 프롬프트 수신 → 승인 → Suno API 작곡 → 트랙 푸시 | | ✍️ **블로그 마케터** (`blog`) | 10:00 매일 | ✅ 발행 | 트렌드 키워드 1개 선택 → 리서치→작가→마케터→평가 자동 실행 → 점수·본문을 텔레그램 승인 요청 → 승인 시 `published` 전환, 거절 시 재생성 | | 🏢 **청약 애널리스트** (`realestate`) | 09:15 매일 | — | realestate-lab 수집 트리거 → 신규 매칭 상위 5건 + 대시보드 요약을 텔레그램 리포트 (읽음 처리 자동) | #### 에이전트별 명령 **Stock** — `fetch_news`, `list_alerts`, `add_alert`, `test_telegram` **Music** — `compose` (승인 필요), `credits` **Blog** — `research {keyword}`, `add_trend_keyword`, `list_trend_keywords` **Realestate** — `fetch_matches`, `dashboard` #### 스케줄러 잡 - 07:00 월요일 — Lotto: AI 큐레이터 브리핑 (5세트 + 내러티브) - 07:30 — Stock: 뉴스 요약 - 09:15 — Realestate: 매칭 리포트 - 10:00 — Blog: 자동 파이프라인 (리서치→생성→리뷰→승인 대기) - 60초 interval — 유휴 에이전트 휴식 체크 ### 7. travel-proxy (`/api/travel/`) 여행 사진 API + SQLite 인덱스 + 온디맨드 썸네일 + 지역 관리. - 원본: `/data/travel/` (RO 마운트) - 썸네일: 480×480 Pillow 리사이징, `/data/thumbs/` 영구 캐시 (tmp → rename 원자성 보장) - DB: `/data/thumbs/travel.db` (photos, album_covers 테이블) - 메타: `region_map.json` (RO) + `region_map_extra.json` (RW 오버라이드) + `regions.geojson` - 지역 관리: 앨범 지역 변경, 커스텀 지역 생성, 지도 핀 좌표 지정 - 데이터 흐름: 수동 sync → 폴더 스캔 → SQLite 인덱싱 + 썸네일 일괄 생성 ### 8. deployer (`/webhook`) Gitea Webhook 수신 → NAS 자동 배포. - HMAC SHA256 서명 검증 (`compare_digest`, `WEBHOOK_SECRET`) - 수신 즉시 200 응답 후 BackgroundTask로 배포 - 배포 스크립트: `git pull` → `.releases/` 백업 → `rsync` → `docker compose up -d --build` → `chown PUID:PGID` - 타임아웃 10분 --- ## 핵심 로직 ### 몬테카를로 시뮬레이션 (lotto-backend) ``` 역대 당첨번호 분석 → 번호별 가중치 산출 → 가중 확률 샘플링으로 후보 20,000개 생성 → 5가지 기법으로 각 조합 점수화 → 상위 100개 DB 저장 → best_picks 20개 교체 ``` | 기법 | 가중치 | 내용 | |------|--------|------| | 빈도 Z-score | 25% | 번호 출현 빈도의 표준편차 | | 조합 지문 | 30% | 합계 정규분포 + 홀짝 비율 + 구간분포 | | 갭 분석 | 20% | 마지막 출현 이후 경과 회차 | | 공동 출현 | 15% | 번호 쌍 동시 출현 빈도 | | 다양성 | 10% | 연속번호·범위·구간 커버리지 | ### LLM 요약 provider 추상화 (stock) `ai_summarizer.py`는 provider 분리 구조. `summarize_news(articles)` 시그니처는 provider와 무관하게 고정. - `_summarize_with_claude`: Anthropic Messages API 직접 호출 (httpx, SDK 의존성 없음) - `_summarize_with_ollama`: Ollama `/api/generate` (타임아웃 180s, qwen3:14b 첫 로드 대응) - 실패 시 `LLMError` (구 `OllamaError` alias 유지) ### 총 자산 스냅샷 (stock) 평일 15:40 자동 실행 → `holidays.json`으로 공휴일 스킵 → 포트폴리오 현재가 조회 + 예수금 합계 → `asset_snapshots` upsert (date UNIQUE). ### 에이전트 FSM + WS 동기화 (agent-office) DB에 저장된 에이전트 상태가 바뀔 때마다 `websocket_manager`가 전체 클라이언트에 브로드캐스트. 텔레그램 봇은 `waiting` 상태 작업에 인라인 키보드를 붙여 승인 요청. 승인/거부 결과가 DB → WS → 프론트로 전파. --- ## 자동 배포 ``` 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에 수동 업로드 (`scripts/deploy.bat --frontend`) --- ## 데이터베이스 각 서비스는 독립 SQLite DB를 `/app/data/` 볼륨에 저장. | DB | 소유 서비스 | 주요 테이블 | |----|------------|-----------| | `lotto.db` | lotto-backend | draws, recommendations, simulation_runs/candidates, best_picks, purchase_history, strategy_performance/weights, weekly_reports, lotto_briefings, todos, blog_posts | | `stock.db` | stock | articles, portfolio, broker_cash, asset_snapshots, sell_history | | `music.db` | music-lab | music_tasks, music_library (provider, lyrics, image_url, suno_id, file_hash, cover_images, wav_url, video_url, stem_urls) | | `blog_marketing.db` | blog-lab | keyword_analyses, blog_posts, brand_links, commissions, generation_tasks, prompt_templates | | `realestate.db` | realestate-lab | announcements, announcement_models, user_profile, match_results, collect_log | | `agent_office.db` | agent-office | agent_config, agent_tasks, agent_logs, telegram_state, conversation_messages | | `travel.db` | travel-proxy | photos (album, filename, mtime, has_thumb), album_covers | --- ## 환경변수 ```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 # LLM (stock, blog-lab, agent-office 공통) ANTHROPIC_API_KEY=sk-ant-... ANTHROPIC_MODEL=claude-haiku-4-5-20251001 LLM_PROVIDER=claude # claude | ollama OLLAMA_URL=http://192.168.45.59:11435 OLLAMA_MODEL=qwen3:14b # music-lab SUNO_API_KEY= MUSIC_AI_SERVER_URL= MUSIC_MEDIA_BASE=/media/music # blog-lab NAVER_CLIENT_ID= NAVER_CLIENT_SECRET= # realestate-lab DATA_GO_KR_API_KEY= # agent-office TELEGRAM_BOT_TOKEN= TELEGRAM_CHAT_ID= TELEGRAM_WEBHOOK_URL= STOCK_URL=http://stock:8000 MUSIC_LAB_URL=http://music-lab:8000 BLOG_LAB_URL=http://blog-lab:8000 REALESTATE_LAB_URL=http://realestate-lab:8000 ``` --- ## 인프라 | 항목 | 값 | |------|----| | 장비 | Synology NAS (Intel Celeron J4025, 18GB RAM) | | Docker | Synology Container Manager | | Git 서버 | Gitea (NAS 내부 self-hosted, `gahusb.synology.me`) | | AI 서버 | Windows PC (192.168.45.59) — RTX 5070 Ti (16GB VRAM) + Ollama + MusicGen | | Python | 3.12 (`slim` 기반 이미지) | | DB | SQLite (볼륨 마운트로 영속 저장) | --- ## 주의사항 - **`.env` 파일** — 절대 커밋 금지. `.env.example`만 레포에 포함 - **Nginx trailing slash** — `/api/portfolio`는 두 location 블록으로 처리 (trailing slash 유무 모두 매칭) - **라우트 순서** — `DELETE /api/todos/done`은 `/api/todos/{id}` 보다 먼저 등록 필수 (FastAPI prefix 매칭) - **캐시 전략** — `index.html`: no-store / `assets/`: 1년 immutable - **PUID/PGID** — travel-proxy는 NAS 파일 권한을 위해 환경변수 주입 필수 - **공휴일 목록** — `stock/app/holidays.json` 매년 수동 갱신 (KRX 기준) - **Windows AI 서버 IP** — `192.168.45.59` 공유기 DHCP 고정 예약. Synology Tailscale은 userspace 모드라 TCP 불가 → 로컬 IP 사용 - **Suno CDN** — `cdn1.suno.ai` URL은 임시 만료 → 생성 즉시 로컬 다운로드 필수 - **LLM provider 롤백** — Claude API 장애 시 `.env`의 `LLM_PROVIDER=ollama`로 전환 후 `docker compose up -d` - **시뮬레이션 교체 방식** — `best_picks`는 교체형 (`is_active=0` 비활성화 후 신규 입력) --- ## 참고 문서 - `CLAUDE.md` — Claude Code 작업용 상세 컨텍스트 (API 전체 목록, 테이블 스키마 등) - `docs/` — 서비스별 기획·설계 문서