# task-1553: 네이버 블로그 비전 기반 UI 자동화 v2 (RAM 효율화)

## 목표
naver_playwright.py에 비전 기반 요소 탐색 기능을 추가한다. RAM 효율적 방식으로 구현.

## 배경
- CSS 셀렉터 방식 4회 실패, 비전 방식 v1은 RAM 99.8%로 강제중단
- 원인: 매 클릭마다 스크린샷(3MB) + base64(4MB) + API 호출 → 메모리 누적
- 서버 RAM: 15GB
- 프로젝트: `/home/jay/projects/BlogAuto`

## 핵심 설계: RAM 효율화 3원칙

### 원칙 1: 비전 API 호출 최소화 (1~2회)
- 글쓰기 페이지 접속 후 **1회만 스크린샷** 촬영
- LLM에게 **모든 UI 요소의 좌표를 한번에 요청**:
  ```
  "이 네이버 블로그 글쓰기 페이지에서 다음 요소들의 중앙 좌표를 JSON으로 알려주세요:
   1. 임시저장 복원 팝업의 취소/새로작성 버튼 (있으면)
   2. 글 제목 입력란
   3. 본문 편집 영역
   4. 발행/임시저장 버튼
   5. 태그 입력란"
  ```
- 팝업 처리 후 에디터 상태가 바뀌면 **1회 추가 스크린샷**으로 좌표 갱신
- **총 비전 API 호출: 최대 2회**

### 원칙 2: 스크린샷 경량화
```python
# PNG(3MB) 대신 JPEG quality 40% (200~300KB)
screenshot_bytes = page.screenshot(type="jpeg", quality=40)
```
- base64 크기도 비례 감소 (4MB → 400KB)
- viewport는 기본 유지 (해상도 낮추면 좌표 정확도 하락)

### 원칙 3: 즉시 메모리 해제
```python
import gc

screenshot_bytes = page.screenshot(type="jpeg", quality=40)
b64 = base64.b64encode(screenshot_bytes).decode()
del screenshot_bytes  # 즉시 해제

coords = _call_vision_api(b64)
del b64  # 즉시 해제
gc.collect()
```

## 구현 요구사항

### 1. `_vision_get_all_coords()` — 전체 좌표 일괄 파악

```python
def _vision_get_all_coords(self, page, elements: list[str]) -> dict[str, tuple[int, int]]:
    """스크린샷 1장으로 여러 요소의 좌표를 한번에 파악한다."""
    screenshot_bytes = page.screenshot(type="jpeg", quality=40)
    b64 = base64.b64encode(screenshot_bytes).decode()
    del screenshot_bytes
    
    elements_text = "\n".join(f"{i+1}. {e}" for i, e in enumerate(elements))
    
    client = anthropic.Anthropic()
    response = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=500,
        messages=[{
            "role": "user",
            "content": [
                {"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": b64}},
                {"type": "text", "text": 
                    f"이 웹페이지 스크린샷에서 다음 요소들의 중앙 좌표를 찾아주세요:\n{elements_text}\n\n"
                    f"JSON으로만 답하세요. 요소가 보이지 않으면 null로 표시:\n"
                    f'{{"1": {{"x": 숫자, "y": 숫자}}, "2": null, ...}}'
                }
            ]
        }]
    )
    del b64
    gc.collect()
    
    # 파싱 후 dict 반환: {"제목 입력란": (x, y), ...}
    raw = json.loads(response.content[0].text)
    result = {}
    for i, elem in enumerate(elements):
        coord = raw.get(str(i+1))
        if coord and coord.get("x") and coord.get("y"):
            result[elem] = (coord["x"], coord["y"])
    return result
```

### 2. `_vision_click()` — 좌표 기반 자연스러운 클릭

```python
def _vision_click_at(self, page, x: int, y: int):
    """좌표로 자연스럽게 마우스 이동 후 클릭."""
    # ⚠️ 절대 순간이동 금지! 반드시 베지어 곡선 이동!
    self._human_mouse_move(page, x, y)
    _random_delay(0.3, 0.8)
    page.mouse.click(x, y)
    _random_delay(0.5, 1.5)
```

### 3. `_vision_type_at()` — 좌표 이동 후 텍스트 입력

```python
def _vision_type_at(self, page, x: int, y: int, text: str):
    """좌표로 이동 → 클릭 → 클립보드 붙여넣기."""
    self._vision_click_at(page, x, y)
    _random_delay(0.3, 0.5)
    
    import subprocess
    subprocess.run(["xclip", "-selection", "clipboard"], input=text.encode(), check=True)
    page.keyboard.press("Control+v")
    _random_delay(0.5, 1.0)
```

### 4. 좌표 캐싱

```python
COORDS_CACHE = "/home/jay/projects/BlogAuto/naver_editor_coords.json"

def _load_cached_coords(self) -> dict | None:
    try:
        with open(COORDS_CACHE) as f:
            return json.load(f)
    except:
        return None

def _save_coords_cache(self, coords: dict):
    with open(COORDS_CACHE, "w") as f:
        json.dump(coords, f, ensure_ascii=False, indent=2)
```
- 첫 실행: 비전으로 좌표 파악 → 캐시 저장
- 이후 실행: 캐시 사용, 클릭 실패 시에만 비전 재분석

### 5. publish() 흐름 수정

```python
# 1. 캐시된 좌표 로드 시도
coords = self._load_cached_coords()

if not coords:
    # 2. 비전으로 전체 좌표 일괄 파악 (API 1회)
    coords = self._vision_get_all_coords(page, [
        "임시저장 복원 팝업의 취소 버튼",
        "블로그 글 제목 입력란",
        "블로그 본문 편집 영역",
        "태그 입력란",
        "임시저장 버튼"
    ])
    self._save_coords_cache(coords)

# 3. 팝업 취소 (있으면) — 마우스 이동 후 클릭
if "임시저장 복원 팝업의 취소 버튼" in coords:
    self._vision_click_at(page, *coords["임시저장 복원 팝업의 취소 버튼"])
    # 팝업 닫힌 후 좌표 갱신 필요 (API 2회째)
    coords = self._vision_get_all_coords(page, [...])

# 4. 제목 입력 — 마우스 이동 후 클릭 후 붙여넣기
self._vision_type_at(page, *coords["블로그 글 제목 입력란"], title)

# 5. 본문 입력
self._vision_type_at(page, *coords["블로그 본문 편집 영역"], body_text)
```

## ⚠️ 절대 규칙
- **마우스 순간이동 절대 금지** — 모든 클릭 전에 `_human_mouse_move()` 베지어 곡선 이동 필수
- **임시저장(draft)만** — public 발행 절대 금지
- **API 키 하드코딩 금지** — .env.keys에서 로드
- Anthropic API 키: `/home/jay/workspace/.env.keys`의 `ANTHROPIC_API_KEY`

## 테스트
1. _vision_get_all_coords 단위 테스트 (메모리 사용량 확인)
2. 좌표 캐싱 동작 확인
3. 글쓰기 페이지 접속 → 팝업 처리 → 제목 입력 → 본문 입력 → 임시저장
4. 기존 테스트(186건) 회귀 방지

## 보고서
`memory/reports/task-1553.md`에 작성