--- name: 2026-04-27 감시종목 웹 뷰 신규 + 종목분석 메일 등락 표기 수정 description: 비하이브 종목분석 메일의 현재가 등락 표기를 매입가 대비로 정정. 감시종목 실시간 웹 뷰(`https://stock.hyowons.net/`)와 launchd 서비스 신규. type: project --- ## 1. 종목분석 메일 등락 표기 버그 수정 ### 증상 관리자님이 받으시는 비하이브 종목분석 메일·텔레그램의 "현재가" 줄에 붙는 등락이 전부 `+0원, +0.00%`으로 잘못 표기됨. ### 원인 `behive_youtube_digest.py:fetch_current_price`가 키움 ka10001의 `change`/`change_pct` 필드(전일대비)를 그대로 출력. 관리자님 의도는 **매입가(`entry['buy']['primary']`) 대비** 등락이었음. ### 수정 - `fetch_current_price(stock, buy_price=None)` 시그니처로 변경. buy_price 주어지면 `(price - buy_primary, pct)` 계산해서 `± N원, ± N% vs 매입가` 표기. 없으면 가격만 표시. - `_buy_primary(entry)` 헬퍼로 `{primary, raw, levels}` dict에서 numeric primary 안전 추출. - 이메일 경로(`format_entry_block`) + 텔레그램 경로(`format_telegram_block`) 둘 다 매입가 전달하도록 수정. 검증: 워치리스트 7종목 전부 매입가 대비 정상 표기 확인 (예: 미래반도체 `21,750원 (+2,750원, +14.47% vs 매입가)`). ## 2. 감시종목 실시간 웹 뷰 신규 ### 동기 관리자님이 매번 레이한테 "워치리스트 어떻게 됐어?" 물어보기 번거로워서 웹페이지로 보고 싶다 요청. 외부망에서도 폰으로 접근 가능해야 함. ### 결정 (대화 누적) | 검토 사항 | 최종 결정 | 이유 | |---|---|---| | 정적 cron 푸시 vs 실시간 서버 | **실시간 Mac 서버** | 페이지 안 볼 땐 호출 0건이라는 관리자님 logic이 cron 모델과 안 맞음 | | 자격증명 위치 | **Mac에만 유지** | 키움 자격증명을 NAS로 옮기지 않음. 워크스페이스 분리 원칙 | | 인증 | **없음** | read-only 페이지, 잔고 노출 없음(워치리스트만), 거래 권한 0. 위생 차원 `noindex,nofollow` + `Referrer-Policy: no-referrer`만 적용 | | URL 형태 | **서브도메인 `stock.hyowons.net`** | 폰 PWA 추가 시 깔끔, path 파싱 부담 0 | | 외부 노출 경로 | **NAS reverse proxy 경유** | NAS가 외부 노출(DDNS) 갖고 있어서 활용. Mac은 LAN/Tailscale 인터페이스만 응답 | ### 구현 - `scripts/behive_web.py` (신규) — Python stdlib `http.server`. `GET /` 한 라우트. watchlist 읽고 키움 ka10001 병렬 호출(8 thread) → 카드형 HTML 반환. JS 0, 인라인 CSS만. 새로고침 버튼은 `` (anchor — 페이지 GET이 곧 키움 새 호출). - `~/Library/LaunchAgents/ai.openclaw.stock.behive-web.plist` (신규) — `KeepAlive=true`, `ThrottleInterval=10`, 부팅 자동 기동. - 모바일 UI: sticky 헤더, safe-area 처리, 40×40 터치 타겟, 720px 이하 1열 레이아웃, PWA 메타(`apple-mobile-web-app-*`, `theme-color`). ### 외부 노출 인프라 ``` 폰/PC → https://stock.hyowons.net/ (Cafe24 도메인) → CNAME → stock.hyowons.duckdns.org (사용자 NAS DDNS) → 공인 IP 112.147.127.79 (NAS WAN) → Synology DSM reverse proxy → http://192.168.219.142:18790 (Mac mini LAN IP) → behive_web.py ``` - **인증서:** Synology Let's Encrypt — `web.hyowons.net` 인증서에 SAN으로 `stock.hyowons.net` 포함 (관리자님 발급 완료). - **Mac 서버 바인딩:** `0.0.0.0:18790` (모든 인터페이스). 외부망 직접 도달은 공유기 NAT 차단으로 보호. - **macOS 방화벽:** Application Firewall ON 상태에서 Python inbound 허용. ### 트러블슈팅 기록 (재발 방지용) NAS → Mac 도달 실패 케이스를 겪었다. 진단 순서: 1. macOS Application Firewall — Python 허용됐는지 (`socketfilterfw --getappblocked /usr/bin/python3`). 2. Tailscale ShieldsUp — `tailscale debug prefs | grep ShieldsUp` 가 false인지. 3. **NAS 호스트 OS의 Tailscale 라우트** — Synology Tailscale 패키지가 컨테이너로 돌면 host shell에 `100.64.0.0/10` 라우트가 안 잡혀서 NAS의 일반 프로세스(reverse proxy nginx 포함)가 Tailscale IP로 outbound 못 함. 4. 우회 방법(현재 채택): **LAN IP 직접 사용** — `192.168.219.142:18790` 으로 reverse proxy 대상 지정. 같은 LAN 세그먼트라 라우팅 문제 0. LAN IP 안정화는 공유기 DHCP 예약(`192.168.219.142` 고정). ## 3. 문서·메모리 갱신 - 루트 `CLAUDE.md` — stock skills/scripts 섹션에 `behive_web.py` 추가 - `agents/stock/workspace/MEMORY.md` — launchd 표에 `ai.openclaw.stock.behive-web` 등록 (상시 실행) ## 4. 운영 노트 - **"감시종목 페이지 어디서 봐?"** 질문에는 → `https://stock.hyowons.net/` - **새로고침 = 키움 ka10001 호출** — 가만히 보고 있으면 키움 호출 0건, 새로고침 누르면 그 시점 가격으로 갱신. - **종목 추가** 흐름은 변경 없음 — 비하이브 신규 영상 자동 감지 또는 수동 `behive_youtube_digest.py add`. watchlist.json 변경되면 다음 페이지 GET부터 자동 반영. - **카드 색상:** 매입가 대비 +면 녹색 좌측 라인, -면 빨간색, 매입가 미설정 또는 0이면 회색. - **"조회 불가"** 카드: 키움 API 실패 시 그 종목만 fallback 표기, 나머지는 정상 렌더 (페이지 전체가 죽지 않음). ## 4-2. 감시 삭제 기능 (2단계) 추가 ### 데이터 모델 워치리스트 entry에 `status`·`pending_delete_at` 필드 도입: - 없음 또는 `"active"` = 정상 감시중 - `"pending_delete"` = 삭제예정 (UI에서 "삭제예정" 섹션, opacity 0.5로 흐리게) ### 웹 라우트 (`behive_web.py`) - `POST /delete` — status=pending_delete + pending_delete_at 기록 - `POST /restore` — status/pending_at 제거 (active 복귀) - `POST /purge` — entry 완전 삭제 (`onsubmit="return confirm()"` 한 번 더 확인) - 모두 form-urlencoded `stock=<종목명>` 받고 303 → `/` redirect - 인증 없음 — 외부 접근 시 NAS reverse proxy + URL 비밀성에 의존, 2단계 자체가 안전망 ### 충돌 방지 가드 | 위치 | 변경 | 이유 | |---|---|---| | `behive_youtube_digest.py:cmd_save` | 기존 entry가 pending_delete면 `mark_seen`만 하고 덮어쓰기 skip | 비하이브 재분석으로 사용자 의도 무력화 방지 | | `watchlist_monitor.py:run` | pending_delete entry는 `continue`로 스킵 | 삭제예정 종목에 buy/target/stop 알림 안 가도록 | 자동 삭제 트리거는 원래 없었음 (모니터·save 둘 다 read 또는 add only). 따라서 자동/수동 충돌은 위 두 가드만으로 해결됨. ### UI 동작 - active 종목 자세히 보기 → "감시 삭제" 버튼 → pending_delete로 이동 (페이지 하단 "삭제예정" 섹션에 흐리게) - pending_delete 종목 자세히 보기 → "복원" / "완전 삭제" 두 버튼. 완전 삭제는 confirm 다이얼로그 한 번 더. ## 매매 절대 원칙 - 이번 작업은 read-only 조회 인프라 + 워치리스트 메타 관리(삭제). 매수·매도 함수 추가 없음. 웹 페이지에 거래 액션 버튼 절대 추가 금지 (조회 전용 원칙).