42 KiB
CLAUDE.md — web-backend 프로젝트 가이드
Claude Code가 이 프로젝트를 작업할 때 참조하는 설정 및 구조 문서.
1. 프로젝트 개요
Synology NAS 기반의 개인 웹 플랫폼 백엔드 모노레포.
- 서비스: lotto-lab, stock, travel-proxy, music-lab, insta-lab, realestate-lab, agent-office, personal, packs-lab, deployer (10개)
- 프론트엔드: 별도 레포 (React + Vite SPA), 빌드 산출물만 NAS에 배포
- 인프라: Docker Compose (10컨테이너) + 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 RTX 5070 Ti (16GB VRAM) + Ollama |
3. NAS 디렉토리 구조
/volume1
├── docker/webpage/ # 운영 런타임 (Docker Compose 실행 위치)
│ ├── lotto/ # lotto 소스 (rsync 동기화)
│ ├── stock/ # stock 소스 (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 |
18000 | 로또 데이터 수집·분석·추천 API |
stock |
18500 | 주식 뉴스·AI 분석·KIS API 연동 |
music-lab |
18600 | AI 음악 생성·라이브러리 관리 API |
insta-lab |
18700 | 인스타 카드 피드 자동 생성 (뉴스→키워드→10페이지 카드) |
realestate-lab |
18800 | 부동산 청약 자동 수집·매칭 API |
agent-office |
18900 | AI 에이전트 오피스 (실시간 WebSocket + 텔레그램 연동) |
packs-lab |
18950 | NAS 자료 다운로드 자동화 (DSM 공유 링크 + 5GB 업로드, Vercel SaaS와 HMAC 통신) |
personal |
18850 | 개인 서비스 (포트폴리오·블로그·투두 통합) |
travel-proxy |
19000 | 여행 사진 API + 썸네일 생성 |
frontend (nginx) |
8080 | 정적 SPA 서빙 + API 리버스 프록시 |
webpage-deployer |
19010 | Gitea Webhook 수신 → 자동 배포 |
5. Nginx 라우팅 규칙
| 경로 | 프록시 대상 | 비고 |
|---|---|---|
/api/ |
lotto:8000 |
lotto API (기본) |
/api/travel/ |
travel-proxy:8000 |
travel API |
/api/stock/ |
stock:8000 |
stock API |
/api/trade/ |
stock:8000 |
KIS 실계좌 API |
/api/portfolio |
stock:8000 |
trailing slash 유무 모두 매칭 |
/api/music/ |
music-lab:8000 |
AI 음악 생성·라이브러리 API |
/api/insta/ |
insta-lab:8000 |
인스타 카드 자동 생성 API |
/api/realestate/ |
realestate-lab:8000 |
부동산 청약 API |
/api/todos |
personal:8000 |
투두 API |
/api/blog/ |
personal:8000 |
블로그 API |
/api/profile/ |
personal:8000 |
포트폴리오 API |
/api/agent-office/ |
agent-office:8000 |
AI 에이전트 오피스 API + WebSocket |
/api/packs/ |
packs-lab:8000 |
5GB 업로드 대응 (client_max_body_size 5G, proxy_request_buffering off, 1800s timeout) |
/webhook, /webhook/ |
deployer:9000 |
Gitea Webhook |
/media/music/ |
/data/music/ (파일 직접 서빙) |
생성된 오디오 파일 |
/media/videos/ |
/data/videos/ (파일 직접 서빙) |
YouTube 영상 MP4 |
/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. 로컬 개발 환경
# .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 |
| Insta Lab | http://localhost:18700 |
| Realestate Lab | http://localhost:18800 |
| Packs Lab | http://localhost:18950 |
9. 서비스별 핵심 정보
lotto-lab (lotto/)
- 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,purchase_manager.py,strategy_evolver.py
lotto.db 테이블
| 테이블 | 설명 |
|---|---|
draws |
로또 당첨번호 |
recommendations |
추천 이력 (즐겨찾기·태그·채점 포함) |
simulation_runs |
시뮬레이션 실행 기록 |
simulation_candidates |
시뮬레이션 후보 (점수 5종) |
best_picks |
현재 활성 최적 번호 20개 (is_active 플래그로 교체) |
purchase_history |
구매 이력 (실제/가상, 번호, 전략 출처, 결과) |
strategy_performance |
전략별 회차 성과 (EMA 입력 데이터) |
strategy_weights |
메타 전략 가중치 (EMA + Softmax) |
weekly_reports |
주간 공략 리포트 캐시 |
lotto_briefings |
AI 큐레이터 주간 브리핑 (5세트 + 내러티브 + 토큰·비용 집계) |
todos |
투두리스트 (UUID PK) — personal 서비스로 이전됨, 레거시 테이블 유지 |
blog_posts |
블로그 글 (tags: JSON 배열) — personal 서비스로 이전됨, 레거시 테이블 유지 |
스케줄러 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 |
배치 추천 저장 |
| GET | /api/lotto/recommend/smart |
전략 진화 기반 메타 추천 |
| GET | /api/lotto/purchase |
구매 이력 조회 (is_real, strategy, draw_no, days 필터) |
| POST | /api/lotto/purchase |
구매 등록 (실제/가상, 번호, 전략 출처 포함) |
| PUT | /api/lotto/purchase/{id} |
구매 이력 수정 |
| DELETE | /api/lotto/purchase/{id} |
구매 이력 삭제 |
| GET | /api/lotto/purchase/stats |
구매 통계 (전체/실제/가상 + 전략별) |
| GET | /api/lotto/strategy/weights |
전략별 가중치 + 성과 + trend |
| GET | /api/lotto/strategy/performance |
전략별 회차 성과 이력 (차트용) |
| POST | /api/lotto/strategy/evolve |
수동 가중치 재계산 |
| 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/lotto/curator/candidates |
큐레이터용 후보 N세트 + 피처 |
| GET | /api/lotto/curator/context |
주간 맥락(핫/콜드·직전 회차) |
| GET | /api/lotto/curator/usage |
큐레이터 토큰·비용 집계 |
| POST | /api/lotto/briefing |
AI 브리핑 저장 |
| GET | /api/lotto/briefing/latest |
최신 브리핑 |
| GET | /api/lotto/briefing/{draw_no} |
특정 회차 브리핑 |
| GET | /api/lotto/briefing |
브리핑 이력 |
stock (stock/)
- 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 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) + YouTube 영상 제작 + 시장 조사 트렌드
- 생성된 오디오 파일:
/app/data/music/(Nginx가/media/music/로 직접 서빙) - 생성된 영상 파일:
/app/data/videos/(Nginx가/media/videos/로 직접 서빙) - DB:
/app/data/music.db(music_tasks, music_library, video_projects, revenue_records, market_trends, trend_reports 테이블) - 파일 구조:
main.py,db.py,suno_provider.py,local_provider.py,video_producer.py,market.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 |
사용 가능한 프로바이더 목록 |
| GET | /api/music/models |
Suno 모델 목록 (V4~V5.5) |
| GET | /api/music/credits |
Suno 크레딧 조회 |
| POST | /api/music/generate |
음악 생성 (provider, model, vocal_gender, negative_tags, style_weight, audio_weight) |
| GET | /api/music/status/{task_id} |
생성 상태 폴링 |
| POST | /api/music/lyrics |
Suno AI 가사 생성 |
| GET | /api/music/library |
라이브러리 전체 조회 |
| POST | /api/music/library |
트랙 수동 추가 |
| DELETE | /api/music/library/{id} |
트랙 삭제 |
| POST | /api/music/extend |
곡 연장 |
| POST | /api/music/vocal-removal |
보컬/인스트 분리 (2트랙) |
| POST | /api/music/cover-image |
커버 이미지 2장 생성 |
| POST | /api/music/wav |
WAV 고음질 변환 |
| POST | /api/music/stem-split |
12스템 분리 (50cr) |
| GET | /api/music/timestamped-lyrics |
타임스탬프 가사 (가라오케) |
| POST | /api/music/style-boost |
AI 스타일 프롬프트 생성 |
| POST | /api/music/upload-cover |
외부 음원 AI Cover |
| POST | /api/music/upload-extend |
외부 음원 확장 |
| POST | /api/music/add-vocals |
인스트에 AI 보컬 추가 |
| POST | /api/music/add-instrumental |
보컬에 AI 반주 추가 |
| POST | /api/music/video |
뮤직비디오 MP4 생성 |
| GET | /api/music/lyrics/library |
저장된 가사 목록 |
| POST | /api/music/lyrics/library |
가사 저장 |
| PUT | /api/music/lyrics/library/{id} |
가사 수정 |
| DELETE | /api/music/lyrics/library/{id} |
가사 삭제 |
| POST | /api/music/video-project |
영상 프로젝트 생성 (track_id, format, target_countries) |
| GET | /api/music/video-projects |
영상 프로젝트 목록 |
| GET | /api/music/video-project/{id} |
영상 프로젝트 상세 |
| POST | /api/music/video-project/{id}/render |
FFmpeg 렌더링 시작 (BackgroundTask) |
| GET | /api/music/video-project/{id}/export |
내보내기 패키지 (mp4+thumbnail+metadata.json) |
| DELETE | /api/music/video-project/{id} |
영상 프로젝트 삭제 |
| GET | /api/music/revenue/dashboard |
수익 대시보드 (총수익·조회수·가중평균 RPM) |
| GET | /api/music/revenue |
수익 기록 목록 |
| POST | /api/music/revenue |
수익 기록 추가 (UNIQUE: yt_video_id+record_month+country) |
| PUT | /api/music/revenue/{id} |
수익 기록 수정 |
| DELETE | /api/music/revenue/{id} |
수익 기록 삭제 |
| POST | /api/music/market/ingest |
agent-office 트렌드 수신 + 리포트 생성 |
| GET | /api/music/market/trends |
트렌드 조회 (country, genre, source, days=7) |
| GET | /api/music/market/report/latest |
최신 트렌드 리포트 |
| GET | /api/music/market/report |
트렌드 리포트 목록 (limit=10) |
| GET | /api/music/market/suggest |
Suno 프롬프트 추천 (limit=5) |
환경변수
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)PEXELS_API_KEY: Pexels 스톡 이미지 API 키 (미설정 시 슬라이드쇼 Pexels 이미지 비활성화)ANTHROPIC_API_KEY: Claude Haiku — YouTube 메타데이터 생성 + 시장 인사이트 (미설정 시 폴백 텍스트)VIDEO_DATA_DIR: 영상 파일 저장 경로 (기본/app/data/videos)
video_projects 테이블
- format:
visualizer|slideshow - status:
pending→rendering→done|failed - target_countries: JSON 배열 (예:
["BR","US"]) - render_params: JSON 객체 (FFmpeg 파라미터 캐시)
revenue_records 테이블
- UNIQUE(yt_video_id, record_month, country)
- avg_rpm 계산: 가중평균
SUM(revenue_usd)/SUM(views)*1000(단순 AVG 아님)
market_trends 테이블
- source:
youtube|google_trends|billboard - metadata: JSON 객체 (원본 API 응답 부분)
- 인덱스:
idx_mt_country_sourceON (country, source, collected_at DESC)
trend_reports 테이블
- report_date UNIQUE — 같은 날 두 번 ingest 시 upsert
- top_genres: JSON 배열
[{genre, score, countries}](최대 10개, score 내림차순) - recommended_styles: JSON 배열
[{genre, suno_prompt, target_countries, reason}](최대 5개)
music_library 테이블 (확장 컬럼)
provider:suno|local— 생성에 사용된 프로바이더lyrics: Suno 생성 가사 텍스트image_url: Suno 생성 커버 이미지 URLsuno_id: Suno 곡 ID (CDN 참조용)file_hash: MD5 해시 (rename 감지용)cover_images: JSON 배열 — 커버 이미지 URL 목록wav_url: WAV 변환 URLvideo_url: 뮤직비디오 URLstem_urls: JSON 객체 — 12스템 URL 맵
Suno 생성 특이사항
- 1회 생성 시 2개 변형(variation) 반환 → 둘 다 라이브러리에 저장
- CDN URL(
cdn1.suno.ai)은 임시 → 반드시 로컬 다운로드 필요 - 가사 섹션 태그:
[Verse],[Chorus],[Bridge],[Instrumental]등
realestate-lab (realestate-lab/)
- 공공데이터포털 API 연동: 한국부동산원 청약홈 분양정보 조회 + 자치구 5티어 매칭 + agent-office push 알림
- DB:
/app/data/realestate.db(announcements, announcement_models, user_profile, match_results, collect_log 테이블) - 파일 구조:
main.py,db.py,collector.py,matcher.py,notifier.py,models.py
환경변수
DATA_GO_KR_API_KEY: 공공데이터포털 API 키 (미설정 시 수동 등록만 가능)AGENT_OFFICE_URL: agent-office 내부 URL (기본http://agent-office:8000) — 신규 매칭 push 대상REALESTATE_NOTIFY_TIMEOUT: agent-office push timeout 초 (기본 15)
스케줄러 job (scheduled_collect 4단계 흐름)
- 09:00 매일 —
collect → cleanup → match → notifycollect_all()— 모집공고일 30일 윈도우(RCRIT_PBLANC_DE_FROM) 사전 좁힘 + 자치구 추출 + status='완료' skipdelete_old_completed_announcements(grace_days=90)—winner_date + 90일경과한 완료 공고 정리 (FK CASCADE로 match_results도 삭제)run_matching()— 자치구 5티어 가중치 + 자격 곡선 적용notify_new_matches()—notified_at IS NULL AND match_score >= profile.min_match_score AND profile.notify_enabled인 매칭을 agent-office로 push
- 00:00 매일 — 상태 갱신 + 재매칭 (
scheduled_status_update, notifier 미호출)
매칭 점수 모델 (총 100점)
- 지역 35점 — 광역 매칭 시 10점 + 자치구 5티어 가중치(S=25 / A=20 / B=15 / C=10 / D=5)
preferred_districts가 모든 티어 비어있으면 광역 매칭만으로 35점 풀 점수 (legacy 호환)
- 주택유형 10점 —
preferred_types에 매칭 (binary) - 면적 15점 —
[min_area, max_area]범위 안 모델 1개 이상 (binary) - 가격 15점 —
max_price이하 모델 1개 이상 (binary) - 자격 25점 —
_check_eligible_types()결과 1개 이상이면 15점 + 추가당 5점, 최대 +10 - reasons 텍스트 예시:
"자치구 S티어: 강남구 (+25)","광역 일치: 서울","선호 지역 일치: 서울"(legacy)
user_profile 신규 컬럼 (Task 2026-04-28 마이그레이션)
preferred_districtsTEXT — JSON{"S":[...], "A":[...], "B":[...], "C":[...], "D":[...]}. default'{}'min_match_scoreINTEGER — 알림 임계값. default 70notify_enabledINTEGER — 알림 ON/OFF. default 1
announcements / match_results 신규 컬럼
announcements.districtTEXT +idx_ann_district인덱스 — collector가 주소/region_name에서 정규식 파싱match_results.notified_atTEXT NULL — agent-office push 성공 시 timestamp 기록 (멱등 마킹)
notifier.py 흐름
get_profile()→notify_enabled=False면 skip,min_match_score가져옴get_unnotified_matches(min_score)— JOIN으로 announcements 정보 포함 (district, status, receipt 등)POST {AGENT_OFFICE_URL}/api/agent-office/realestate/notifybody={"matches": [...]}- 응답
{sent_ids: [...]}→mark_matches_notified(sent_ids)(notified_at = now) - RequestException 시 마킹 안 함 → 다음 사이클 재시도
realestate-lab API 목록
| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | /api/realestate/announcements |
공고 목록. 응답에 district, match_score, match_reasons, eligible_types 포함 |
| GET | /api/realestate/announcements/{id} |
공고 상세 (주택형별 + district 포함) |
| POST | /api/realestate/announcements |
수동 공고 등록 |
| PUT | /api/realestate/announcements/{id} |
공고 수정 |
| PATCH | /api/realestate/announcements/{id}/bookmark |
북마크 토글 (텔레그램 인라인 키보드 콜백 대상) |
| DELETE | /api/realestate/announcements/{id} |
공고 삭제 |
| DELETE | /api/realestate/announcements/closed |
status='완료' 공고 일괄 삭제 |
| POST | /api/realestate/collect |
수동 수집 트리거 (collect → cleanup → match → notify 전체 흐름) |
| GET | /api/realestate/collect/status |
마지막 수집 결과 |
| GET | /api/realestate/profile |
내 프로필 조회 (preferred_districts, min_match_score, notify_enabled 포함) |
| PUT | /api/realestate/profile |
프로필 수정 (upsert). body에 preferred_districts: {S:[],...}, min_match_score: 0~100, notify_enabled: bool 수용 |
| GET | /api/realestate/matches |
매칭 결과 목록 (응답에 district, status 포함) |
| POST | /api/realestate/matches/refresh |
매칭 재계산 |
| PATCH | /api/realestate/matches/{id}/read |
신규 알림 읽음 처리 |
| GET | /api/realestate/dashboard |
요약 (진행중 공고수, 신규 매칭수, 다가오는 일정) |
travel-proxy (travel-proxy/)
- 원본 사진:
/data/travel/(RO) - 썸네일 캐시:
/data/thumbs/(RW) - DB:
/data/thumbs/travel.db(photos, album_covers 테이블) - 메타:
/data/travel/_meta/region_map.json,regions.geojson - 지역 오버라이드:
/data/thumbs/region_map_extra.json(RW,_regions_meta포함) - 파일 구조:
main.py,db.py,indexer.py - 썸네일: 480×480 리사이징 (Pillow), 동기화 시 사전 생성 + 온디맨드 폴백
- 데이터 흐름: 수동 sync → 폴더 스캔 → SQLite 인덱싱 + 썸네일 일괄 생성
travel.db 테이블
| 테이블 | 설명 |
|---|---|
photos |
사진 인덱스 (album, filename, mtime, has_thumb) |
album_covers |
앨범별 커버 사진 지정 |
지역 관리 아키텍처
region_map.json(RO): 원본 지역→앨범 매핑 (_meta/안에 위치)region_map_extra.json(RW): 사용자 수정분 오버라이드 (앨범 이동, 신규 지역)_regions_meta: 커스텀 지역의 이름·좌표 저장 ({ "region_id": { "name": "...", "coordinates": [lng, lat] } })
regions.geojson(RO): GeoJSON Polygon 지역 경계- 커스텀 지역:
GET /api/travel/regions에서region_map에 있지만 GeoJSON에 없는 지역을 자동 추가 (Point geometry 또는 null)
travel-proxy API 목록
| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | /api/travel/regions |
지역 GeoJSON (커스텀 지역 동적 추가 포함) |
| GET | /api/travel/photos |
사진 목록 (region, page=1, size=20) |
| POST | /api/travel/sync |
폴더 스캔 → DB 동기화 + 썸네일 생성 |
| GET | /api/travel/albums |
앨범 목록 + 사진 수 + 커버 + region/regionName |
| PUT | /api/travel/albums/{album}/cover |
앨범 커버 지정 |
| PUT | /api/travel/albums/{album}/region |
앨범 지역 변경 (region_map_extra 수정) |
| PUT | /api/travel/regions/{region_id} |
커스텀 지역 이름/좌표 수정 (지도 핀 표시용) |
insta-lab (insta-lab/)
- 인스타그램 카드 피드 자동 생성 — 뉴스 모니터링 → 키워드 추출 → 10페이지 카드 카피 + PNG 렌더 → 텔레그램 푸시 → 사용자 수동 업로드
- DB:
/app/data/insta.db(news_articles, trending_keywords, card_slates, card_assets, generation_tasks, prompt_templates) - 카드 사이즈: 1080×1350 (인스타 4:5 세로)
- 카드 렌더: Jinja2 템플릿 → Playwright headless Chromium 스크린샷
- 파일 구조:
app/main.py,config.py,db.py,news_collector.py,keyword_extractor.py,card_writer.py,card_renderer.py,templates/default/card.html.j2
환경변수
NAVER_CLIENT_ID/NAVER_CLIENT_SECRET: 네이버 검색 APIANTHROPIC_API_KEY: Claude API (Haiku=키워드 정제, Sonnet=카드 카피)ANTHROPIC_MODEL_HAIKU/ANTHROPIC_MODEL_SONNET: 모델명 오버라이드INSTA_DATA_PATH: SQLite + 카드 PNG 저장 경로 (기본/app/data)CARD_TEMPLATE_DIR: HTML 템플릿 디렉토리 (기본/app/app/templates)NEWS_PER_CATEGORY/KEYWORDS_PER_CATEGORY: 수집·추출 limit 튜닝
카테고리 시드 키워드
- 기본 economy / psychology / celebrity 3종 (config.DEFAULT_CATEGORY_SEEDS)
prompt_templates.name='category_seeds'에 JSON으로 오버라이드 가능
카드 슬레이트 (card_slates)
- status:
draft→rendered→sent(또는failed) - cover_copy / body_copies (8개) / cta_copy / suggested_caption / hashtags JSON 컬럼
- accent_color는 카테고리별 기본값 (economy=#0F62FE, psychology=#A66CFF, celebrity=#FF5C8A)
스케줄러 job (agent-office)
- 09:30 매일 —
_run_insta_schedule(insta_pipeline) → 뉴스 수집 → 키워드 추출 → 텔레그램 후보 푸시 agent_config.custom_config.auto_select=True이면 카테고리당 1위 키워드 자동 슬레이트 생성·발송
insta-lab API 목록
| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | /api/insta/status |
서비스 상태 (NAVER/ANTHROPIC 키 여부) |
| POST | /api/insta/news/collect |
뉴스 수집 트리거 (BackgroundTask) |
| GET | /api/insta/news/articles |
수집 기사 목록 (category, days) |
| POST | /api/insta/keywords/extract |
키워드 추출 트리거 (BackgroundTask) |
| GET | /api/insta/keywords |
트렌딩 키워드 목록 (category, used) |
| POST | /api/insta/slates |
슬레이트 생성 (keyword, category) |
| GET | /api/insta/slates |
슬레이트 목록 |
| GET | /api/insta/slates/{id} |
슬레이트 상세 + 자산 |
| POST | /api/insta/slates/{id}/render |
카드 렌더 재시도 |
| GET | /api/insta/slates/{id}/assets/{page} |
카드 PNG 다운로드 (1~10) |
| DELETE | /api/insta/slates/{id} |
슬레이트 삭제 (자산 파일 포함) |
| GET | /api/insta/tasks/{task_id} |
BackgroundTask 상태 폴링 |
| GET/PUT | /api/insta/templates/prompts/{name} |
프롬프트 템플릿 CRUD |
agent-office (agent-office/)
- AI 에이전트 가상 오피스 — 2D 픽셀아트 사무실에서 에이전트가 실제 작업 수행
- stock/music-lab/realestate-lab 기존 API를 서비스 프록시로 호출 (직접 DB 접근 없음)
- 실시간 상태 동기화: WebSocket (
/api/agent-office/ws) - 텔레그램 봇: 양방향 알림 + 승인 (인라인 키보드)
- 청약 매칭 알림: realestate-lab이 신규 매칭 발견 시 push →
RealestateAgent.on_new_matches()→ 텔레그램 1통(인라인 [🔖 북마크]/[📄 공고] 또는 [전체 보기] 버튼) - DB:
/app/data/agent_office.db(agent_config, agent_tasks, agent_logs, telegram_state 테이블) - 파일 구조:
main.py,db.py,config.py,models.py,websocket_manager.py,service_proxy.py,telegram_bot.py,scheduler.py,agents/base.py,agents/stock.py,agents/music.py,agents/realestate.py,telegram/realestate_message.py
에이전트 FSM 상태: idle → working → waiting (승인 대기) → reporting → break (휴식)
환경변수
STOCK_URL: stock 내부 URL (기본http://stock:8000)MUSIC_LAB_URL: music-lab 내부 URL (기본http://music-lab:8000)REALESTATE_LAB_URL: realestate-lab 내부 URL (기본http://realestate-lab:8000) — 북마크 콜백 프록시 대상REALESTATE_DASHBOARD_URL: 텔레그램 [전체 보기] 버튼 URL (기본http://localhost:8080/realestate)TELEGRAM_BOT_TOKEN: 텔레그램 봇 토큰 (미설정 시 알림 비활성화)TELEGRAM_CHAT_ID: 텔레그램 채팅 IDTELEGRAM_WEBHOOK_URL: 텔레그램 Webhook URLTELEGRAM_WIFE_CHAT_ID: 아내 chat.id (브리핑 공유 + 대화 허용)ANTHROPIC_API_KEY: 자연어 대화용 Claude API 키 (미설정 시 대화 비활성)CONVERSATION_MODEL: 대화 모델 (기본claude-haiku-4-5-20251001)CONVERSATION_HISTORY_LIMIT: 이력 주입 수 (기본 20)CONVERSATION_RATE_PER_MIN: 채팅당 분당 최대 메시지 (기본 6)LOTTO_BACKEND_URL: 기본http://lotto:8000LOTTO_CURATOR_MODEL: 기본claude-sonnet-4-5YOUTUBE_DATA_API_KEY: YouTube Data API v3 키 (미설정 시 YouTube trending 수집 skip)
YouTubeResearchAgent (agents/youtube.py)
agent_id = "youtube"— AGENT_REGISTRY에 등록- 09:00 매일
on_schedule()→ 국가별 YouTube 트렌딩 + Google Trends + Billboard Top20 수집 → music-lab push on_command("research", {countries: []})→ 수동 트리거 (백그라운드 asyncio.create_task)- 수집 소스:
youtube_researcher.py(fetch_youtube_trending, fetch_google_trends, fetch_billboard_top20) - DB:
youtube_research_jobs테이블에 실행 이력 기록 - 동시실행 방지:
self.state == "working"체크 후 거부 - 월요일 08:00
send_weekly_report()→ music-lab 최신 리포트 → 텔레그램 발송
텔레그램 자연어 대화 (옵션 B)
- 슬래시 명령이 아닌 일반 문장을 보내면 Claude Haiku 4.5가 응답
- 프롬프트 캐싱:
system블록 + 히스토리 마지막 블록에cache_control: ephemeral→ 5분 TTL - 허용 chat_id 화이트리스트:
TELEGRAM_CHAT_ID,TELEGRAM_WIFE_CHAT_ID - 평가 지표:
conversation_messages테이블에 tokens / cache_read / cache_write / latency 기록 - 조회:
GET /api/agent-office/conversation/stats?days=7
스케줄러 job
- 07:30 매일 — 주식 뉴스 요약 (
stock_news_job) - 매주 월요일 07:00 — 로또 큐레이터 브리핑 (
lotto_curate) - 60초 간격 — 유휴 에이전트 휴식 체크 (
idle_check_job) 09:15 매일 — 청약 매칭 데일리 리포트(Task 2026-04-28에서 폐기. realestate-lab의 push 트리거로 전환)- 09:00 매일 — YouTube 트렌드 수집 (
youtube_research) → music-lab/api/music/market/ingestpush - 매주 월요일 08:00 — YouTube 주간 리포트 텔레그램 발송 (
youtube_weekly_report)
RealestateAgent (agents/realestate.py)
- 진입점:
on_new_matches(matches: list[dict]) -> {sent, sent_ids, message_id} - realestate-lab의 push에서 트리거 →
format_realestate_matches()+build_match_keyboard()→messaging.send_raw() - 1~2건이면 풀 카드 + [🔖 북마크]/[📄 공고 보기] 행씩, 3건 이상이면 묶음 카드 + [📋 전체 보기] 단일 URL 버튼
- 인라인 키보드 콜백
realestate_bookmark_{id}→webhook.py의_handle_realestate_bookmark→service_proxy.realestate_bookmark_toggle()→ realestate-lab의PATCH /announcements/{id}/bookmark - 송신 성공 시 sent_ids 반환 → realestate-lab이 match_results.notified_at 마킹 (멱등)
- 실패 시 sent=0/sent_ids=[]/error 반환 → 마킹 안 됨 → 다음 사이클 재시도
on_command("fetch_matches"): 수동 트리거 — service_proxy로 매치 가져와on_new_matches호출on_schedule: 폐기 (cron 등록 제거됨)
agent-office API 목록
| 메서드 | 경로 | 설명 |
|---|---|---|
| WS | /api/agent-office/ws |
WebSocket (init, agent_state, task_complete, command_result) |
| GET | /api/agent-office/agents |
에이전트 목록 |
| GET | /api/agent-office/agents/{id} |
에이전트 상세 (설정 + 상태) |
| PUT | /api/agent-office/agents/{id} |
에이전트 설정 수정 |
| GET | /api/agent-office/agents/{id}/tasks |
에이전트 작업 이력 |
| GET | /api/agent-office/agents/{id}/logs |
에이전트 로그 |
| GET | /api/agent-office/tasks/pending |
승인 대기 작업 목록 |
| GET | /api/agent-office/tasks/{id} |
작업 상세 |
| POST | /api/agent-office/command |
에이전트에 명령 전송 |
| POST | /api/agent-office/approve |
작업 승인/거부 |
| POST | /api/agent-office/telegram/webhook |
텔레그램 Webhook 수신 (realestate_bookmark_* 콜백 포함) |
| POST | /api/agent-office/realestate/notify |
realestate-lab 전용 push 수신 → 텔레그램 송신 |
| GET | /api/agent-office/states |
전체 에이전트 상태 조회 |
| GET | /api/agent-office/conversation/stats |
텔레그램 자연어 대화 토큰·캐시 통계 (days 필터) |
| POST | /api/agent-office/youtube/research |
YouTube 트렌드 수집 수동 트리거 (body: {countries: []}) |
| GET | /api/agent-office/youtube/research/status |
마지막 수집 작업 상태 |
personal (personal/)
- 개인 서비스 (포트폴리오 + 블로그 + 투두 통합)
- DB:
/app/data/personal.db(profile, careers, projects, skills, introductions, todos, blog_posts 테이블) - 편집 인증:
PORTFOLIO_EDIT_PASSWORD환경변수, Bearer 토큰 (24시간 TTL) - 파일 구조:
main.py,db.py,models.py,auth.py
환경변수
PORTFOLIO_EDIT_PASSWORD: 편집 모드 비밀번호 (미설정 시 편집 불가)
personal API 목록
| 메서드 | 경로 | 설명 |
|---|---|---|
| GET | /api/profile/public |
공개 데이터 일괄 조회 |
| POST | /api/profile/auth |
비밀번호 인증 → 토큰 |
| GET | /api/profile/profile |
프로필 조회 (인증) |
| PUT | /api/profile/profile |
프로필 수정 (인증) |
| GET | /api/profile/careers |
경력 목록 (인증) |
| POST | /api/profile/careers |
경력 추가 (인증) |
| PUT | /api/profile/careers/{id} |
경력 수정 (인증) |
| DELETE | /api/profile/careers/{id} |
경력 삭제 (인증) |
| GET | /api/profile/projects |
프로젝트 목록 (인증) |
| POST | /api/profile/projects |
프로젝트 추가 (인증) |
| PUT | /api/profile/projects/{id} |
프로젝트 수정 (인증) |
| DELETE | /api/profile/projects/{id} |
프로젝트 삭제 (인증) |
| GET | /api/profile/skills |
기술 목록 (인증) |
| POST | /api/profile/skills |
기술 추가 (인증) |
| PUT | /api/profile/skills/{id} |
기술 수정 (인증) |
| DELETE | /api/profile/skills/{id} |
기술 삭제 (인증) |
| GET | /api/profile/introductions |
자기소개 목록 (인증) |
| POST | /api/profile/introductions |
자기소개 추가 (인증) |
| PUT | /api/profile/introductions/{id} |
자기소개 수정 (인증) |
| DELETE | /api/profile/introductions/{id} |
자기소개 삭제 (인증) |
| PATCH | /api/profile/introductions/{id}/main |
메인 자기소개 지정 (인증) |
| GET | /api/todos |
투두 전체 목록 |
| POST | /api/todos |
투두 생성 |
| PUT | /api/todos/{id} |
투두 수정 |
| DELETE | /api/todos/done |
완료 항목 일괄 삭제 |
| DELETE | /api/todos/{id} |
투두 개별 삭제 |
| GET | /api/blog/posts |
블로그 글 목록 |
| POST | /api/blog/posts |
블로그 글 생성 |
| PUT | /api/blog/posts/{id} |
블로그 글 수정 |
| DELETE | /api/blog/posts/{id} |
블로그 글 삭제 |
packs-lab (packs-lab/)
- NAS 자료 다운로드 자동화 — Synology DSM 공유링크 발급 + 5GB 멀티파트 업로드 수신
- Vercel SaaS와 HMAC 인증으로 통신, 사용자 인증은 Vercel이 Supabase로 처리 (본 서비스는 외부 인증 없음)
- DB: 외부 Supabase
pack_files테이블 (DDL:packs-lab/supabase/pack_files.sql) - 파일 구조:
app/main.py,app/auth.py,app/dsm_client.py,app/routes.py,app/models.py - 경로 3분리:
PACK_DATA_PATH(호스트 OS path, docker volume 좌측) →PACK_BASE_DIR(컨테이너 내부, upload 저장 target) →PACK_HOST_DIR(DSM API path, Supabase에 저장). 운영 NAS에서PACK_HOST_DIR미설정 시 sign-link가 컨테이너 경로를 DSM에 전달해 파일을 못 찾음. - ⚠️ DSM API path 형식: Synology DSM API는 일반 사용자 권한일 때
/<shared_folder>/...형식만 인식하고/volume1/...절대경로는 거부(error 408). 운영 NAS는 반드시PACK_HOST_DIR=/docker/webpage/media/packs(shared folder 시점) 설정. admin 사용자만/volume1/...사용 가능하나 보안상 권장 안 함.
환경변수
DSM_HOST/DSM_USER/DSM_PASS: Synology DSM 7.x 인증 (공유 링크 발급용)DSM_VERIFY_SSL: SSL 검증 (defaulttrue). LAN IP + self-signed cert 환경에서 IP mismatch 시false설정 (LAN 내부 통신이라 허용)BACKEND_HMAC_SECRET: Vercel SaaS와 양쪽 공유 시크릿 (HMAC SHA256)SUPABASE_URL/SUPABASE_SERVICE_KEY: Supabase pack_files 테이블 접근 (service_role, RLS 우회)UPLOAD_TOKEN_TTL_SEC: admin upload 토큰 TTL (기본 1800초 = 30분)PACK_BASE_DIR: 컨테이너 내부 저장 경로 (기본/app/data/packs)PACK_HOST_DIR: DSM API용 path. 운영 NAS는/docker/webpage/media/packs(shared folder 시점). 미설정 시PACK_BASE_DIR로 fallback (DSM 호출 X 환경에서만 안전)PACK_DATA_PATH: docker-compose volume 마운트의 호스트 측 OS 경로 (로컬./data/packs, NAS/volume1/docker/webpage/media/packs)
HMAC 인증 패턴
- Vercel → backend 요청:
X-Timestamp(UNIX 초) +X-Signature(HMAC_SHA256(timestamp + "." + body, secret)) - Replay 방어: 타임스탬프 ±5분 윈도우
- admin browser → backend upload:
Authorization: Bearer <token>(jti 단발성)
packs-lab API 목록
| 메서드 | 경로 | 설명 |
|---|---|---|
| POST | /api/packs/sign-link |
Vercel HMAC → DSM Sharing.create로 4시간 유효 다운로드 URL 발급 |
| POST | /api/packs/admin/mint-token |
Vercel HMAC → 일회성 upload 토큰 발급 (기본 30분 TTL) |
| POST | /api/packs/upload |
Bearer token (single-shot) → multipart 5GB 저장 + Supabase INSERT |
| POST | /api/packs/upload/init |
Bearer token → chunked upload 세션 초기화 (session_id = jti, chunk_max_size 반환). init만 jti consume |
| PUT | /api/packs/upload/{session_id}/chunk?offset=N |
동일 Bearer token → 부분파일 append (offset 불일치 시 409 + X-Current-Offset 헤더) |
| GET | /api/packs/upload/{session_id}/status |
동일 Bearer token → {written, expected_size} 조회 (재개용) |
| POST | /api/packs/upload/{session_id}/complete |
동일 Bearer token → 부분파일 rename + Supabase INSERT |
| DELETE | /api/packs/upload/{session_id} |
동일 Bearer token → 세션 중단 + 부분파일 정리 |
| GET | /api/packs/list |
Vercel HMAC → 활성 pack_files 목록 (deleted_at IS NULL) |
| DELETE | /api/packs/{file_id} |
Vercel HMAC → soft delete (DSM 공유는 자동 만료) |
Chunked upload 흐름 (5GB+ 안정성)
- 같은 mint-token을 init·chunk·status·complete·abort 전체에서 Bearer로 재사용 (jti consume은 init에서만)
- 세션 state: 컨테이너 내부
PACK_BASE_DIR/.uploads/{jti}/meta.json + data.part - chunk 재시도: 클라이언트는 PUT 응답 헤더
X-Current-Offset또는GET /status로 재개 지점 확인 - 환경변수
PACK_CHUNK_MAX_SIZE(기본 64MB) — 너무 크면 nginx buffering 부담, 너무 작으면 RTT 비용
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}보다 반드시 먼저 등록 (personal 서비스, FastAPI prefix 매칭 순서) - PUID/PGID: travel-proxy는 NAS 파일 권한을 위해 PUID/PGID를 환경변수로 주입
- 캐시 전략:
index.html은no-store,assets/는 1년 장기 캐시(immutable) - Frontend 배포: git push로 자동 배포되지 않음. 로컬 빌드 후 NAS에 수동 업로드
- .env 파일: 절대 커밋 금지.
.env.example만 레포에 포함 - 공휴일 목록:
stock/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으로 비활성화 후 신규 입력 - insta-lab Playwright: NAS에서 chromium 빌드는 가능하지만 +500MB 이미지. 메모리 부족 시 카드 렌더 실패 가능 — 한 번에 1슬레이트만 렌더하도록 직렬화됨