# 2026-05-04

- 키움 REST 조회 중 `일반` 계좌에서 `8005: Token이 유효하지 않습니다` 오류 재현.
- 원인 확인: 토큰 캐시 만료시각은 남아 있었지만 키움 서버에서 해당 토큰을 무효 처리한 상태.
- 즉시 조치: `python3 scripts/kiwoom_client.py token 일반`로 재발급 후 `balance 일반` 정상 조회 확인.
- 코드 보강: `scripts/kiwoom_client.py`의 `_call()`에서 `8005` 또는 `Token이 유효하지 않습니다` 응답 시 해당 계좌 토큰을 `force=True`로 재발급하고 동일 요청 1회 자동 재시도하도록 수정.
- 검증: `state/kiwoom_tokens/일반.json`에 의도적으로 가짜 토큰을 넣은 뒤 `python3 scripts/kiwoom_client.py balance 일반` 실행 → 자동 재발급 후 정상 조회 성공.

## behive_web.py 대공사 (코디 작업)

기존 워치리스트 단일 페이지 → **감시종목 / 관리자 / 가희 3개 탭** 구조로 전면 개편. 세부 변경:

### 데이터 레이어
- `_fetch_all_data()` 도입 — `kt00018(보유)·kt00001(예수금)×4계좌·ka10170(당일매매일지)·ka10001(워치리스트 시세)`를 단일 ThreadPoolExecutor로 병렬 호출. 한 분기 실패해도 다른 분기 계속.
- `_build_owner_data()` — `stock_portfolio_report`에서 `consolidate / aggregate_journal / group_by_owner / OWNER_LABELS / SNAPSHOT_FILE / _normalize_prev_snap / load_json` 임포트 재활용. 단일 진실 출처.
- 보유 종목의 `day_change`는 web 경로에선 ka10001 보정 생략 (rate limit·렌더 속도). kt00018 raw 사용 — 장마감 후엔 0으로 채워지는 한계 인지.

### 당일 평가손익 (KPI 카드)
- 첫 시도였던 `Σ(day_change×qty for held) + Σ(pl_amt for phantoms)` 방식은 보유 종목의 round-trip 실현분 누락 + 신규 매수분 과대계상으로 부정확 → **`portfolio_daily_snapshot.json`의 prev day 값 기반 `total_net - prev_net`**로 변경. 미실현+실현+현금흐름 모두 포함.
- 비교 일자(`YYYY-MM-DD 대비`)도 함께 표시. 스냅샷 없으면 `'전날 스냅샷 없음'` graceful.
- 즉, **stock.briefing 메일의 일일 P&L과 동일한 ground truth 사용**. 메일·웹 수치 일치.

### 탭 구현 (FOUC 방지 v3)
- v1 라디오 버튼 → v2 `:target/:has` → v3 **`html[data-tab]` attribute selector**로 정착.
- `<head>` 안 sync inline script가 `location.hash` 읽어 body 렌더 **전**에 `documentElement.dataset.tab` 설정 → 첫 paint부터 정답 탭. 새로고침해도 깜빡임 없음.
- `hashchange` 리스너로 탭 클릭 후 즉시 동기화. URL fragment 기반이라 `https://stock.hyowons.net/#tab-self` 직링크도 가능.

### topbar + pull-to-refresh
- 헤더+nav를 `.topbar` 단일 sticky div로 통합. 기존 `top:57px` 하드코딩 의존 제거 (iOS notch 환경에서 헤더 높이가 더 길어져도 nav가 헤더 뒤로 숨던 버그 해결).
- `.page` wrapper로 topbar+컨텐츠 전체 묶음 → 당김 제스처 시 통째로 `translateY` → 타이틀까지 같이 내려옴, 그 위로 빈공간 노출.
- `.page.dragging .topbar { position: static }`으로 당기는 동안만 topbar sticky 일시 해제.
- ptr 인디케이터: 노치 회피 위해 `top: calc(-52px + env(safe-area-inset-top, 0px))`, `z-index:25`로 topbar(z:20)보다 위. 기본 opacity:0, 당김 d/25로 빠른 페이드인.
- 당김 인터랙션: resistance 0.55, 임계 70px 넘으면 `↓` 화살표 → 빨강 spinner(원호 70%, 0.7s linear infinite) 전환 + `location.reload()`. 못 넘으면 cubic-bezier로 spring back.
- `overscroll-behavior-y: none`으로 iOS rubber band 차단해 시각 충돌 제거.
- 헤더 타이틀에 `refresh-flash` keyframe (빨강→평상색 0.8s) — 페이지 로드 1회 발동으로 "방금 갱신됨" 신호.

### 새로고침 버튼
- 기존 `href="/"` (URL fragment 손실) → `<a href="#" onclick="event.preventDefault();location.reload();">`로 변경. 현재 탭 유지하며 데이터만 재요청.

### 영향 범위
- launchd plist 변경 없음. 데몬 재기동만 (`ai.openclaw.stock.behive-web` bootout/bootstrap)으로 새 코드 로드.
- 키움 호출량 약간 증가: 페이지 GET 1회당 `kt00018×1 + kt00001×4 + ka10170×4 + ka10001×N(워치리스트)`. 평소 사용 빈도 고려 시 무시 가능 수준.
- 외부 노출 URL `https://stock.hyowons.net/`는 그대로. 인증 없음·read-only 원칙 유지 (`scripts/behive_web.py`에 주문 함수 부재).
