@kood/claude-code 0.5.10 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +117 -67
- package/package.json +1 -1
- package/templates/.claude/agents/build-fixer.md +371 -0
- package/templates/.claude/agents/critic.md +223 -0
- package/templates/.claude/agents/deep-executor.md +320 -0
- package/templates/.claude/agents/git-operator.md +15 -0
- package/templates/.claude/agents/planner.md +11 -7
- package/templates/.claude/agents/qa-tester.md +488 -0
- package/templates/.claude/agents/researcher.md +189 -0
- package/templates/.claude/agents/scientist.md +544 -0
- package/templates/.claude/agents/security-reviewer.md +549 -0
- package/templates/.claude/agents/tdd-guide.md +413 -0
- package/templates/.claude/agents/vision.md +165 -0
- package/templates/.claude/commands/pre-deploy.md +79 -2
- package/templates/.claude/instructions/agent-patterns/model-routing.md +2 -2
- package/templates/.claude/skills/brainstorm/SKILL.md +889 -0
- package/templates/.claude/skills/bug-fix/SKILL.md +69 -0
- package/templates/.claude/skills/crawler/SKILL.md +156 -0
- package/templates/.claude/skills/crawler/references/anti-bot-checklist.md +162 -0
- package/templates/.claude/skills/crawler/references/code-templates.md +119 -0
- package/templates/.claude/skills/crawler/references/crawling-patterns.md +167 -0
- package/templates/.claude/skills/crawler/references/document-templates.md +147 -0
- package/templates/.claude/skills/crawler/references/network-crawling.md +141 -0
- package/templates/.claude/skills/crawler/references/playwriter-commands.md +172 -0
- package/templates/.claude/skills/crawler/references/pre-crawl-checklist.md +221 -0
- package/templates/.claude/skills/crawler/references/selector-strategies.md +140 -0
- package/templates/.claude/skills/execute/SKILL.md +5 -0
- package/templates/.claude/skills/feedback/SKILL.md +570 -0
- package/templates/.claude/skills/figma-to-code/SKILL.md +1 -0
- package/templates/.claude/skills/global-uiux-design/SKILL.md +1 -0
- package/templates/.claude/skills/korea-uiux-design/SKILL.md +1 -0
- package/templates/.claude/skills/nextjs-react-best-practices/SKILL.md +1 -0
- package/templates/.claude/skills/plan/SKILL.md +44 -0
- package/templates/.claude/skills/ralph/SKILL.md +16 -18
- package/templates/.claude/skills/refactor/SKILL.md +19 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/SKILL.md +1 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# 분석 결과 문서 템플릿
|
|
2
|
+
|
|
3
|
+
> `.claude/crawler/[사이트명]/` 폴더 구조
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 폴더 구조
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
.claude/crawler/example-com/
|
|
11
|
+
├── ANALYSIS.md # 사이트 구조
|
|
12
|
+
├── SELECTORS.md # DOM selector
|
|
13
|
+
├── API.md # API endpoint
|
|
14
|
+
├── NETWORK.md # 인증 정보
|
|
15
|
+
└── CRAWLER.ts # 생성 코드
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## ANALYSIS.md
|
|
21
|
+
|
|
22
|
+
```markdown
|
|
23
|
+
# [사이트명] 크롤링 분석
|
|
24
|
+
|
|
25
|
+
생성: {{TIMESTAMP}}
|
|
26
|
+
URL: {{BASE_URL}}
|
|
27
|
+
|
|
28
|
+
## 페이지 타입
|
|
29
|
+
|
|
30
|
+
| 타입 | URL | 비고 |
|
|
31
|
+
|------|-----|------|
|
|
32
|
+
| 목록 | /items | |
|
|
33
|
+
| 상세 | /items/[id] | |
|
|
34
|
+
| 검색 | /search?q= | |
|
|
35
|
+
|
|
36
|
+
## 데이터 로딩
|
|
37
|
+
|
|
38
|
+
- [ ] SSR
|
|
39
|
+
- [ ] CSR (API 기반)
|
|
40
|
+
- [ ] 무한 스크롤
|
|
41
|
+
- [ ] 페이지네이션
|
|
42
|
+
|
|
43
|
+
## 인증
|
|
44
|
+
|
|
45
|
+
- [ ] 공개
|
|
46
|
+
- [ ] 로그인 필요
|
|
47
|
+
- [ ] API 키
|
|
48
|
+
|
|
49
|
+
## 발견사항
|
|
50
|
+
|
|
51
|
+
[특이사항, 제약, 주의점]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## SELECTORS.md
|
|
57
|
+
|
|
58
|
+
```markdown
|
|
59
|
+
# [사이트명] Selector
|
|
60
|
+
|
|
61
|
+
## 목록 페이지
|
|
62
|
+
|
|
63
|
+
| 요소 | Selector | 비고 |
|
|
64
|
+
|------|----------|------|
|
|
65
|
+
| 컨테이너 | `.item-list` | |
|
|
66
|
+
| 카드 | `.item-card` | |
|
|
67
|
+
| 제목 | `.item-card h2` | |
|
|
68
|
+
| 링크 | `.item-card a` | |
|
|
69
|
+
| 다음 | `button[aria-label="Next"]` | |
|
|
70
|
+
|
|
71
|
+
## aria-ref 매핑
|
|
72
|
+
|
|
73
|
+
| ref | Selector | 설명 |
|
|
74
|
+
|-----|----------|------|
|
|
75
|
+
| e14 | `getByRole('button', { name: 'Load more' })` | 더보기 |
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## API.md
|
|
81
|
+
|
|
82
|
+
```markdown
|
|
83
|
+
# [사이트명] API
|
|
84
|
+
|
|
85
|
+
## GET /api/items
|
|
86
|
+
|
|
87
|
+
**Parameters:**
|
|
88
|
+
|
|
89
|
+
| 파라미터 | 타입 | 설명 |
|
|
90
|
+
|----------|------|------|
|
|
91
|
+
| page | number | 페이지 |
|
|
92
|
+
| limit | number | 개수 |
|
|
93
|
+
|
|
94
|
+
**Headers:**
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{ "Authorization": "Bearer ..." }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Response:**
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
interface Response {
|
|
104
|
+
data: Item[];
|
|
105
|
+
pagination: { page: number; total: number; hasNext: boolean };
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Rate Limiting
|
|
110
|
+
|
|
111
|
+
[제한사항]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## NETWORK.md
|
|
117
|
+
|
|
118
|
+
```markdown
|
|
119
|
+
# [사이트명] Network
|
|
120
|
+
|
|
121
|
+
## 인증 정보
|
|
122
|
+
|
|
123
|
+
| 항목 | 값 | 만료 |
|
|
124
|
+
|------|-----|------|
|
|
125
|
+
| Cookie | `session=...` | 24h |
|
|
126
|
+
| Token | `Bearer ...` | 1h |
|
|
127
|
+
|
|
128
|
+
## 필수 헤더
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"Cookie": "...",
|
|
133
|
+
"Authorization": "Bearer ...",
|
|
134
|
+
"User-Agent": "Mozilla/5.0 ..."
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Rate Limit
|
|
139
|
+
|
|
140
|
+
- 제한: 60 req/min
|
|
141
|
+
- 딜레이: 1000ms
|
|
142
|
+
|
|
143
|
+
## 봇 탐지
|
|
144
|
+
|
|
145
|
+
- [ ] Cloudflare
|
|
146
|
+
- [ ] reCAPTCHA
|
|
147
|
+
```
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Network 분석
|
|
2
|
+
|
|
3
|
+
> Playwriter로 인증 정보 추출 → NETWORK.md 문서화
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<workflow>
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
1. Playwriter로 페이지 탐방
|
|
11
|
+
2. API 인터셉트 → 엔드포인트/헤더/쿠키/토큰 추출
|
|
12
|
+
3. NETWORK.md에 문서화
|
|
13
|
+
4. 크롤러 코드 작성 시 활용
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
</workflow>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
<cookie>
|
|
21
|
+
|
|
22
|
+
## 쿠키 추출
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# 모든 쿠키
|
|
26
|
+
playwriter -s 1 -e "console.log(JSON.stringify(await context.cookies(), null, 2))"
|
|
27
|
+
|
|
28
|
+
# 인증 쿠키만
|
|
29
|
+
playwriter -s 1 -e $'
|
|
30
|
+
const cookies = await context.cookies();
|
|
31
|
+
console.log(cookies.filter(c =>
|
|
32
|
+
["session","token","auth","sid"].some(n => c.name.toLowerCase().includes(n))
|
|
33
|
+
));
|
|
34
|
+
'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
</cookie>
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
<token>
|
|
42
|
+
|
|
43
|
+
## 토큰 추출
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# localStorage
|
|
47
|
+
playwriter -s 1 -e "console.log(await state.page.evaluate(() => localStorage.getItem('token')))"
|
|
48
|
+
|
|
49
|
+
# sessionStorage
|
|
50
|
+
playwriter -s 1 -e "console.log(await state.page.evaluate(() => sessionStorage.getItem('accessToken')))"
|
|
51
|
+
|
|
52
|
+
# Authorization 헤더
|
|
53
|
+
playwriter -s 1 -e $'
|
|
54
|
+
state.page.on("request", req => {
|
|
55
|
+
const auth = req.headers()["authorization"];
|
|
56
|
+
if (auth) console.log("Auth:", auth);
|
|
57
|
+
});
|
|
58
|
+
'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
</token>
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
<headers>
|
|
66
|
+
|
|
67
|
+
## 헤더 캡처
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
playwriter -s 1 -e $'
|
|
71
|
+
state.page.on("request", req => {
|
|
72
|
+
if (req.url().includes("/api/"))
|
|
73
|
+
console.log(JSON.stringify(req.headers(), null, 2));
|
|
74
|
+
});
|
|
75
|
+
'
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
</headers>
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
<bot_check>
|
|
83
|
+
|
|
84
|
+
## 봇 탐지 확인
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
playwriter -s 1 -e "console.log(await state.page.content().then(c => c.includes('cf-')))"
|
|
88
|
+
|
|
89
|
+
playwriter -s 1 -e $'
|
|
90
|
+
state.page.on("response", res => {
|
|
91
|
+
if ([403, 429].includes(res.status()))
|
|
92
|
+
console.log("차단:", res.status(), res.url());
|
|
93
|
+
});
|
|
94
|
+
'
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
</bot_check>
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
<template>
|
|
102
|
+
|
|
103
|
+
## NETWORK.md 템플릿
|
|
104
|
+
|
|
105
|
+
```markdown
|
|
106
|
+
# [사이트명] Network
|
|
107
|
+
|
|
108
|
+
## 인증
|
|
109
|
+
|
|
110
|
+
| 항목 | 값 | 만료 |
|
|
111
|
+
|------|-----|------|
|
|
112
|
+
| Cookie | `session=...` | 24h |
|
|
113
|
+
| Token | `Bearer ...` | 1h |
|
|
114
|
+
|
|
115
|
+
## 필수 헤더
|
|
116
|
+
|
|
117
|
+
\`\`\`json
|
|
118
|
+
{
|
|
119
|
+
"Cookie": "...",
|
|
120
|
+
"Authorization": "Bearer ...",
|
|
121
|
+
"User-Agent": "Mozilla/5.0 ..."
|
|
122
|
+
}
|
|
123
|
+
\`\`\`
|
|
124
|
+
|
|
125
|
+
## Rate Limit
|
|
126
|
+
|
|
127
|
+
- 제한: 60 req/min
|
|
128
|
+
- 딜레이: 1000ms
|
|
129
|
+
|
|
130
|
+
## 봇 탐지
|
|
131
|
+
|
|
132
|
+
- [ ] Cloudflare
|
|
133
|
+
- [ ] reCAPTCHA
|
|
134
|
+
|
|
135
|
+
## 참고
|
|
136
|
+
|
|
137
|
+
- 봇 탐지 강함 → Nstbrowser
|
|
138
|
+
- 쿠키 만료 짧음 → 갱신 로직
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
</template>
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Playwriter 명령어
|
|
2
|
+
|
|
3
|
+
> 크롤링 분석용 핵심 명령어
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<session>
|
|
8
|
+
|
|
9
|
+
## 세션
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
playwriter session new # 생성 → ID 반환
|
|
13
|
+
playwriter session list # 목록
|
|
14
|
+
playwriter session reset 1 # 리셋
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
</session>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<execution>
|
|
22
|
+
|
|
23
|
+
## 실행
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
playwriter -s 1 -e "<code>"
|
|
27
|
+
playwriter -s 1 --timeout 20000 -e "<code>" # 타임아웃 증가
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
</execution>
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
<page>
|
|
35
|
+
|
|
36
|
+
## 페이지
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
// 생성 + 이동
|
|
40
|
+
state.page = await context.newPage();
|
|
41
|
+
await state.page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
|
|
42
|
+
|
|
43
|
+
// 기존 페이지 찾기
|
|
44
|
+
state.page = context.pages().find(p => p.url().includes('example.com'));
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
</page>
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
<structure>
|
|
52
|
+
|
|
53
|
+
## 구조 파악
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
// 접근성 트리 (권장)
|
|
57
|
+
await accessibilitySnapshot({ page: state.page })
|
|
58
|
+
await accessibilitySnapshot({ page: state.page, search: /button|link/ })
|
|
59
|
+
|
|
60
|
+
// 시각적 확인
|
|
61
|
+
await screenshotWithAccessibilityLabels({ page: state.page })
|
|
62
|
+
|
|
63
|
+
// HTML
|
|
64
|
+
await getCleanHTML({ locator: state.page.locator('body') })
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
</structure>
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
<interaction>
|
|
72
|
+
|
|
73
|
+
## 상호작용
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
// 클릭
|
|
77
|
+
await page.locator('aria-ref=e14').click()
|
|
78
|
+
await page.getByRole('button', { name: 'Submit' }).click()
|
|
79
|
+
|
|
80
|
+
// 입력
|
|
81
|
+
await page.locator('input[name="email"]').fill('test@example.com')
|
|
82
|
+
|
|
83
|
+
// 스크롤
|
|
84
|
+
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight))
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
</interaction>
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
<selector>
|
|
92
|
+
|
|
93
|
+
## Selector 추출
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
// aria-ref → Playwright selector
|
|
97
|
+
const sel = await getLocatorStringForElement(page.locator('aria-ref=e14'));
|
|
98
|
+
// => "getByRole('button', { name: 'Save' })"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
</selector>
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
<network>
|
|
106
|
+
|
|
107
|
+
## 네트워크 인터셉트
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
state.requests = []; state.responses = [];
|
|
111
|
+
page.on('request', req => {
|
|
112
|
+
if (req.url().includes('/api/'))
|
|
113
|
+
state.requests.push({ url: req.url(), method: req.method(), headers: req.headers() });
|
|
114
|
+
});
|
|
115
|
+
page.on('response', async res => {
|
|
116
|
+
if (res.url().includes('/api/'))
|
|
117
|
+
try { state.responses.push({ url: res.url(), body: await res.json() }); } catch {}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// 분석
|
|
121
|
+
state.responses.forEach(r => console.log(r.url));
|
|
122
|
+
|
|
123
|
+
// 정리
|
|
124
|
+
page.removeAllListeners('request');
|
|
125
|
+
page.removeAllListeners('response');
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
</network>
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
<screenshot>
|
|
133
|
+
|
|
134
|
+
## 스크린샷
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
await page.screenshot({ path: 'shot.png', scale: 'css' })
|
|
138
|
+
await page.screenshot({ path: 'full.png', scale: 'css', fullPage: true })
|
|
139
|
+
await page.locator('.card').screenshot({ path: 'card.png', scale: 'css' })
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
</screenshot>
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
<wait>
|
|
147
|
+
|
|
148
|
+
## 대기
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
await page.waitForLoadState('domcontentloaded')
|
|
152
|
+
await page.waitForSelector('.loaded')
|
|
153
|
+
await waitForPageLoad({ page: state.page, timeout: 5000 })
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
</wait>
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
<utils>
|
|
161
|
+
|
|
162
|
+
## 유틸리티
|
|
163
|
+
|
|
164
|
+
| 함수 | 용도 |
|
|
165
|
+
|------|------|
|
|
166
|
+
| `accessibilitySnapshot({ page })` | 접근성 트리 |
|
|
167
|
+
| `screenshotWithAccessibilityLabels({ page })` | 레이블 스크린샷 |
|
|
168
|
+
| `getCleanHTML({ locator })` | 정제된 HTML |
|
|
169
|
+
| `getLocatorStringForElement(locator)` | Selector 추출 |
|
|
170
|
+
| `waitForPageLoad({ page })` | 로드 대기 |
|
|
171
|
+
|
|
172
|
+
</utils>
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# 크롤링 전 분석 체크리스트
|
|
2
|
+
|
|
3
|
+
> Playwriter 사이트 분석 시 확인 항목
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<rendering_check>
|
|
8
|
+
|
|
9
|
+
## 렌더링 방식
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
playwriter -s 1 -e $'
|
|
13
|
+
const html = await state.page.content();
|
|
14
|
+
console.log("HTML:", html.length, "| SSR:", html.length > 5000 ? "가능" : "CSR");
|
|
15
|
+
'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| 방식 | 특징 | 전략 |
|
|
19
|
+
|------|------|------|
|
|
20
|
+
| SSR | HTML에 데이터 포함 | DOM 파싱 |
|
|
21
|
+
| CSR | JS로 로딩 | API 인터셉트 |
|
|
22
|
+
| Hybrid | 초기 SSR + 추가 CSR | 혼합 |
|
|
23
|
+
|
|
24
|
+
</rendering_check>
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<bot_detection>
|
|
29
|
+
|
|
30
|
+
## 봇 탐지 확인
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
playwriter -s 1 -e $'
|
|
34
|
+
const html = await state.page.content();
|
|
35
|
+
console.log({
|
|
36
|
+
cloudflare: html.includes("cf-ray") || html.includes("cf-challenge"),
|
|
37
|
+
recaptcha: html.includes("recaptcha"),
|
|
38
|
+
hcaptcha: html.includes("hcaptcha"),
|
|
39
|
+
datadome: html.includes("datadome"),
|
|
40
|
+
akamai: html.includes("_abck"),
|
|
41
|
+
});
|
|
42
|
+
'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# 차단 응답 모니터링
|
|
47
|
+
playwriter -s 1 -e $'
|
|
48
|
+
state.page.on("response", res => {
|
|
49
|
+
if ([403, 429, 503].includes(res.status()))
|
|
50
|
+
console.log("차단:", res.status(), res.url());
|
|
51
|
+
});
|
|
52
|
+
'
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
| 코드 | 의미 | 대응 |
|
|
56
|
+
|------|------|------|
|
|
57
|
+
| 403 | 접근 차단 | Anti-Detect |
|
|
58
|
+
| 429 | Rate Limit | 딜레이 증가 |
|
|
59
|
+
| 503 | 일시 차단 | 대기 후 재시도 |
|
|
60
|
+
|
|
61
|
+
</bot_detection>
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
<honeypot>
|
|
66
|
+
|
|
67
|
+
## 허니팟 탐지
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
playwriter -s 1 -e $'
|
|
71
|
+
const hidden = await state.page.$$eval("a", links =>
|
|
72
|
+
links.filter(a => {
|
|
73
|
+
const s = getComputedStyle(a);
|
|
74
|
+
return s.display==="none" || s.visibility==="hidden" || s.opacity==="0" || a.offsetWidth===0;
|
|
75
|
+
}).map(a => a.href)
|
|
76
|
+
);
|
|
77
|
+
console.log("숨겨진 링크:", hidden.length);
|
|
78
|
+
'
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
| 유형 | 탐지 | 대응 |
|
|
82
|
+
|------|------|------|
|
|
83
|
+
| 숨김 링크 | `display:none`, `visibility:hidden` | 클릭 금지 |
|
|
84
|
+
| 함정 필드 | `name*=honeypot`, `name*=trap` | 입력 금지 |
|
|
85
|
+
| 0x0 요소 | `offsetWidth===0` | 무시 |
|
|
86
|
+
|
|
87
|
+
</honeypot>
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
<rate_limit>
|
|
92
|
+
|
|
93
|
+
## Rate Limiting
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
playwriter -s 1 -e $'
|
|
97
|
+
state.page.on("response", res => {
|
|
98
|
+
const h = res.headers();
|
|
99
|
+
if (h["x-ratelimit-limit"]) console.log("Limit:", h["x-ratelimit-limit"]);
|
|
100
|
+
if (h["retry-after"]) console.log("Retry:", h["retry-after"]);
|
|
101
|
+
});
|
|
102
|
+
'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
| 헤더 | 설명 |
|
|
106
|
+
|------|------|
|
|
107
|
+
| `X-RateLimit-Limit` | 최대 요청 수 |
|
|
108
|
+
| `X-RateLimit-Remaining` | 남은 요청 수 |
|
|
109
|
+
| `Retry-After` | 대기 시간 (초) |
|
|
110
|
+
|
|
111
|
+
</rate_limit>
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
<auth_check>
|
|
116
|
+
|
|
117
|
+
## 인증 분석
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# 쿠키
|
|
121
|
+
playwriter -s 1 -e $'
|
|
122
|
+
const cookies = await context.cookies();
|
|
123
|
+
console.log(cookies.filter(c =>
|
|
124
|
+
["session","token","auth","jwt"].some(k => c.name.toLowerCase().includes(k))
|
|
125
|
+
));
|
|
126
|
+
'
|
|
127
|
+
|
|
128
|
+
# 토큰
|
|
129
|
+
playwriter -s 1 -e $'
|
|
130
|
+
const storage = await state.page.evaluate(() => ({
|
|
131
|
+
local: Object.keys(localStorage).filter(k => k.match(/token|auth|jwt/i)),
|
|
132
|
+
session: Object.keys(sessionStorage).filter(k => k.match(/token|auth|jwt/i))
|
|
133
|
+
}));
|
|
134
|
+
console.log(storage);
|
|
135
|
+
'
|
|
136
|
+
|
|
137
|
+
# Authorization 헤더
|
|
138
|
+
playwriter -s 1 -e $'
|
|
139
|
+
state.page.on("request", req => {
|
|
140
|
+
const auth = req.headers()["authorization"];
|
|
141
|
+
if (auth) console.log("Auth:", auth.slice(0, 50));
|
|
142
|
+
});
|
|
143
|
+
'
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
</auth_check>
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
<api_discovery>
|
|
151
|
+
|
|
152
|
+
## API 발견
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
playwriter -s 1 -e $'
|
|
156
|
+
state.apis = [];
|
|
157
|
+
state.page.on("request", req => {
|
|
158
|
+
if (req.url().includes("/api/") || req.resourceType()==="fetch")
|
|
159
|
+
state.apis.push({ method: req.method(), url: req.url().split("?")[0] });
|
|
160
|
+
});
|
|
161
|
+
'
|
|
162
|
+
# 페이지 탐색 후
|
|
163
|
+
playwriter -s 1 -e $'
|
|
164
|
+
const unique = [...new Map(state.apis.map(a => [a.url, a])).values()];
|
|
165
|
+
console.log(unique);
|
|
166
|
+
'
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
</api_discovery>
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
<dynamic_content>
|
|
174
|
+
|
|
175
|
+
## 동적 콘텐츠
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# Lazy loading
|
|
179
|
+
playwriter -s 1 -e $'
|
|
180
|
+
const lazy = await state.page.$$eval("img", imgs =>
|
|
181
|
+
imgs.filter(i => i.dataset.src || i.loading==="lazy").length
|
|
182
|
+
);
|
|
183
|
+
console.log("Lazy 이미지:", lazy);
|
|
184
|
+
'
|
|
185
|
+
|
|
186
|
+
# Shadow DOM / iframe
|
|
187
|
+
playwriter -s 1 -e $'
|
|
188
|
+
const shadow = await state.page.$$eval("*", els => els.filter(e => e.shadowRoot).length);
|
|
189
|
+
const iframes = await state.page.$$eval("iframe", f => f.length);
|
|
190
|
+
console.log("Shadow:", shadow, "| iframe:", iframes);
|
|
191
|
+
'
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
</dynamic_content>
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
<decision_table>
|
|
199
|
+
|
|
200
|
+
## 전략 결정
|
|
201
|
+
|
|
202
|
+
| 항목 | 결과 | 권장 |
|
|
203
|
+
|------|------|------|
|
|
204
|
+
| 렌더링 | SSR / CSR | DOM / API |
|
|
205
|
+
| 봇 탐지 | 없음 / 있음 | 일반 / Anti-Detect |
|
|
206
|
+
| 인증 | 공개 / 필요 | 직접 / 쿠키·토큰 |
|
|
207
|
+
| Rate Limit | 없음 / 있음 | 병렬 / 딜레이 |
|
|
208
|
+
|
|
209
|
+
</decision_table>
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
<warnings>
|
|
214
|
+
|
|
215
|
+
```text
|
|
216
|
+
⚠️ 허니팟 클릭 금지
|
|
217
|
+
⚠️ Rate Limit 초과 시 중단
|
|
218
|
+
⚠️ robots.txt 확인
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
</warnings>
|