# 1️⃣ 설계 원칙

### 핵심 원칙 5가지

1. **Playwright 의존 코드는 한 곳으로 격리**
2. **사이트별 로직은 모듈로 분리**
3. **페이지 조작과 데이터 추출을 분리**
4. **공통 브라우저/네트워크/세션 로직은 재사용**
5. **CLI / LLM / 스케줄러가 호출하기 쉬운 형태**

---

# 2️⃣ 프로젝트 구조 (Node.js + Playwright)

```text
pw-crawler/
├─ package.json
├─ playwright.config.js          # 전역 설정 (선택)
├─ .env
├─ src/
│  ├─ core/                      # 🔥 Playwright 추상화 계층
│  │  ├─ browser.js              # 브라우저/컨텍스트 생성
│  │  ├─ network.js              # request/response 로깅
│  │  ├─ context.js              # 공통 context 옵션
│  │  ├─ storage.js
│  │  └─ errors.js
│  │
│  ├─ pages/                     # 🔥 Page Object (페이지 조작)
│  │  ├─ common/
│  │  │  └─ base.page.js
│  │  ├─ siteA/
│  │  │  ├─ login.page.js
│  │  │  ├─ list.page.js
│  │  │  └─ detail.page.js
│  │  └─ siteB/
│  │     └─ ...
│  │
│  ├─ extractors/                # 🔥 DOM → 데이터 추출
│  │  ├─ siteA.extractor.js
│  │  └─ siteB.extractor.js
│  │
│  ├─ flows/                     # 🔥 크롤링 시나리오
│  │  ├─ siteA.flow.js
│  │  └─ siteB.flow.js
│  │
│  ├─ services/                  # 저장/전송 계층
│  │  ├─ saveToFile.js
│  │  ├─ sendToApi.js
│  │  └─ normalize.js
│  │
│  ├─ cli/                       # CLI 진입점
│  │  └─ crawl.js
│  │
│  └─ index.js                   # 프로그래매틱 진입점
│
├─ data/                         # 결과물
│  ├─ raw/
│  └─ normalized/
│
└─ logs/
```

---

# 3️⃣ 각 디렉토리의 역할 (중요)

## 🔥 `core/` — Playwright 의존성 “봉인”

> **여기만 Playwright를 직접 알게 한다**

### browser.js

```js
const { chromium } = require('playwright');

async function createBrowser() {
  return chromium.launch({
    headless: false,
    slowMo: 100
  });
}

module.exports = { createBrowser };
```

👉 나중에 **headless/브라우저 변경**해도 여기만 수정

---

## 🔥 `pages/` — Page Object (행동만 담당)

> “이 페이지에서 뭘 할 수 있나?”

```js
class ListPage {
  constructor(page) {
    this.page = page;
  }

  async open() {
    await this.page.goto('https://siteA.com/list');
  }

  async clickFirstItem() {
    await this.page.locator('.item').first().click();
  }
}

module.exports = { ListPage };
```

❌ 데이터 파싱 금지
⭕ 클릭/이동/입력만

---

## 🔥 `extractors/` — 데이터만 추출

> DOM → JSON

```js
async function extractItems(page) {
  return page.$$eval('.item', els =>
    els.map(el => ({
      title: el.innerText,
      link: el.href
    }))
  );
}

module.exports = { extractItems };
```

👉 페이지 구조 변경 시 여기만 수정

---

## 🔥 `flows/` — 크롤링 시나리오 (핵심)

> **“어떤 순서로 뭘 할 것인가”**

```js
const { ListPage } = require('../pages/siteA/list.page');
const { extractItems } = require('../extractors/siteA.extractor');

async function crawlSiteA(context) {
  const page = await context.newPage();
  const listPage = new ListPage(page);

  await listPage.open();
  const items = await extractItems(page);

  return items;
}

module.exports = { crawlSiteA };
```

👉 사이트별 로직은 여기서 끝

---

## 🔥 `services/` — 결과 처리

* 파일 저장
* DB 저장
* API 전송
* 데이터 정규화

크롤링 로직과 **완전히 분리**하는 게 중요

---

## 🔥 `cli/` — 실행 진입점

```js
#!/usr/bin/env node
const { createBrowser } = require('../core/browser');
const { crawlSiteA } = require('../flows/siteA.flow');

(async () => {
  const browser = await createBrowser();
  const context = await browser.newContext();

  const data = await crawlSiteA(context);
  console.log(data);

  await browser.close();
})();
```

---

# 4️⃣ “여러 사이트” 대응 전략 (확장성)

### 사이트 늘릴 때 해야 할 일

* `pages/siteC/*`
* `extractors/siteC.extractor.js`
* `flows/siteC.flow.js`

👉 **core / services는 그대로**

---

# 5️⃣ 실무에서 특히 중요한 추가 포인트

## ① 네트워크 로깅은 core로

```text
core/network.js
```

* 모든 page에 공통 적용
* 필요 시 on/off 플래그

---

## ② 에러 복구 전략

* 페이지 단위 try/catch
* 실패 URL 재큐잉
* 스크린샷 자동 저장

---

## ③ CLI / LLM 친화적 구조

```bash
node cli/crawl.js siteA --headless
```

또는:

```js
runCrawl({ site: 'siteA', mode: 'debug' });
```

---
