Files
web-page-backend/CLAUDE.md

43 KiB
Raw Blame History

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_latestcheck_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: pendingrenderingdone | 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_source ON (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 생성 커버 이미지 URL
  • suno_id: Suno 곡 ID (CDN 참조용)
  • file_hash: MD5 해시 (rename 감지용)
  • cover_images: JSON 배열 — 커버 이미지 URL 목록
  • wav_url: WAV 변환 URL
  • video_url: 뮤직비디오 URL
  • stem_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 → notify
    1. collect_all() — 모집공고일 30일 윈도우(RCRIT_PBLANC_DE_FROM) 사전 좁힘 + 자치구 추출 + status='완료' skip
    2. delete_old_completed_announcements(grace_days=90)winner_date + 90일 경과한 완료 공고 정리 (FK CASCADE로 match_results도 삭제)
    3. run_matching() — 자치구 5티어 가중치 + 자격 곡선 적용
    4. 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_districts TEXT — JSON {"S":[...], "A":[...], "B":[...], "C":[...], "D":[...]}. default '{}'
  • min_match_score INTEGER — 알림 임계값. default 70
  • notify_enabled INTEGER — 알림 ON/OFF. default 1

announcements / match_results 신규 컬럼

  • announcements.district TEXT + idx_ann_district 인덱스 — collector가 주소/region_name에서 정규식 파싱
  • match_results.notified_at TEXT NULL — agent-office push 성공 시 timestamp 기록 (멱등 마킹)

notifier.py 흐름

  1. get_profile()notify_enabled=False면 skip, min_match_score 가져옴
  2. get_unnotified_matches(min_score) — JOIN으로 announcements 정보 포함 (district, status, receipt 등)
  3. POST {AGENT_OFFICE_URL}/api/agent-office/realestate/notify body={"matches": [...]}
  4. 응답 {sent_ids: [...]}mark_matches_notified(sent_ids) (notified_at = now)
  5. 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: 네이버 검색 API
  • ANTHROPIC_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)
  • INSTA_DEFAULT_THEME: 카드 렌더에 사용할 theme 디렉토리명 (기본 default). templates/<theme>/card.html.j2가 없으면 자동으로 default 폴백
  • NEWS_PER_CATEGORY / KEYWORDS_PER_CATEGORY: 수집·추출 limit 튜닝

카테고리 시드 키워드

  • 기본 economy / psychology / celebrity 3종 (config.DEFAULT_CATEGORY_SEEDS)
  • prompt_templates.name='category_seeds'에 JSON으로 오버라이드 가능

카드 슬레이트 (card_slates)

  • status: draftrenderedsent (또는 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위 키워드 자동 슬레이트 생성·발송

디자인 import (사용자 디자인 PNG → Claude Vision → Jinja HTML 자동 생성)

  • insta-lab/app/templates/<theme>/pages/*.png (10장, 1080×1350, placeholder 텍스트 박혀있는 형태) → Claude Sonnet Vision → templates/<theme>/card.html.j2 자동 생성
  • CLI: docker exec insta-lab python -m app.design_importer <theme>
  • 파일명 자동 매핑: cover/start/intro → page 1, cta/outro/finish/end → page 10, 나머지 알파벳 순 → page 2~9
  • 매핑 override: pages/_order.json{filename: page_no} 명시 (10장 + page 1~10 완전 매핑일 때만 적용)
  • Vision prompt에 placeholder 마스킹 요구 포함 (2-layer: 마스킹 박스 + 동적 텍스트 layer)
  • 기존 HTML 자동 백업 (card.html.j2.bak.YYYYMMDD-HHMMSS)
  • Jinja 문법 깨진 응답은 card.html.j2.error.txt로 보존 + ValueError
  • 활성화: NAS .envINSTA_DEFAULT_THEME=<theme> 추가 + docker compose restart insta-lab
  • 토큰 비용: 1회당 15K tokens ($0.05 Sonnet 기준)

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: 텔레그램 채팅 ID
  • TELEGRAM_WEBHOOK_URL: 텔레그램 Webhook URL
  • TELEGRAM_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:8000
  • LOTTO_CURATOR_MODEL: 기본 claude-sonnet-4-5
  • YOUTUBE_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/ingest push
  • 매주 월요일 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_bookmarkservice_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 검증 (default true). 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/doneDELETE /api/todos/{id} 보다 반드시 먼저 등록 (personal 서비스, FastAPI prefix 매칭 순서)
  • PUID/PGID: travel-proxy는 NAS 파일 권한을 위해 PUID/PGID를 환경변수로 주입
  • 캐시 전략: index.htmlno-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슬레이트만 렌더하도록 직렬화됨