@kood/claude-code 0.5.9 → 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 +127 -135
- 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/dependency-manager.md +0 -1
- package/templates/.claude/agents/deployment-validator.md +0 -1
- package/templates/.claude/agents/designer.md +0 -1
- package/templates/.claude/agents/document-writer.md +0 -1
- package/templates/.claude/agents/git-operator.md +15 -0
- package/templates/.claude/agents/implementation-executor.md +0 -1
- package/templates/.claude/agents/ko-to-en-translator.md +0 -1
- package/templates/.claude/agents/lint-fixer.md +0 -1
- 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
- package/templates/.claude/skills/stitch-design/README.md +0 -34
- package/templates/.claude/skills/stitch-design/SKILL.md +0 -213
- package/templates/.claude/skills/stitch-design/examples/DESIGN.md +0 -154
- package/templates/.claude/skills/stitch-loop/README.md +0 -54
- package/templates/.claude/skills/stitch-loop/SKILL.md +0 -316
- package/templates/.claude/skills/stitch-loop/examples/SITE.md +0 -73
- package/templates/.claude/skills/stitch-loop/examples/next-prompt.md +0 -25
- package/templates/.claude/skills/stitch-loop/resources/baton-schema.md +0 -61
- package/templates/.claude/skills/stitch-loop/resources/site-template.md +0 -104
- package/templates/.claude/skills/stitch-react/README.md +0 -36
- package/templates/.claude/skills/stitch-react/SKILL.md +0 -323
- package/templates/.claude/skills/stitch-react/examples/gold-standard-card.tsx +0 -88
- package/templates/.claude/skills/stitch-react/package-lock.json +0 -231
- package/templates/.claude/skills/stitch-react/package.json +0 -16
- package/templates/.claude/skills/stitch-react/resources/architecture-checklist.md +0 -15
- package/templates/.claude/skills/stitch-react/resources/component-template.tsx +0 -37
- package/templates/.claude/skills/stitch-react/resources/stitch-api-reference.md +0 -14
- package/templates/.claude/skills/stitch-react/resources/style-guide.json +0 -24
- package/templates/.claude/skills/stitch-react/scripts/fetch-stitch.sh +0 -30
- package/templates/.claude/skills/stitch-react/scripts/validate.js +0 -77
|
@@ -359,6 +359,10 @@ Task(subagent_type="architect", model="opus", ...)
|
|
|
359
359
|
| 검증 | code-reviewer | opus | 수정 후 코드 리뷰, 회귀 검증 |
|
|
360
360
|
| 린트 | lint-fixer | sonnet | tsc/eslint 오류 수정 |
|
|
361
361
|
| 문서 | document-writer | haiku/sonnet | 버그 리포트, 수정 내역 문서화 |
|
|
362
|
+
| 보안 | security-reviewer | opus | 보안 취약점 버그, SQL Injection, XSS |
|
|
363
|
+
| 테스트 | qa-tester | sonnet | 수정 후 CLI/서비스 테스트 검증 |
|
|
364
|
+
| 조사 | researcher | sonnet | 외부 라이브러리 버그, API 문서 조사 |
|
|
365
|
+
| 시각 | vision | sonnet | UI 버그 스크린샷 분석, 레이아웃 검증 |
|
|
362
366
|
|
|
363
367
|
### Bug Severity별 병렬 처리
|
|
364
368
|
|
|
@@ -803,6 +807,71 @@ Task({
|
|
|
803
807
|
// → 총 소요 시간: 15-18분
|
|
804
808
|
```
|
|
805
809
|
|
|
810
|
+
#### 예시 5: 보안 버그 수정 + 전체 스캔
|
|
811
|
+
|
|
812
|
+
**상황:** SQL Injection 취약점 발견 및 유사 취약점 스캔 필요
|
|
813
|
+
|
|
814
|
+
```typescript
|
|
815
|
+
// ✅ 보안 버그 수정 + 검증 병렬
|
|
816
|
+
Task({
|
|
817
|
+
subagent_type: 'implementation-executor',
|
|
818
|
+
model: 'sonnet',
|
|
819
|
+
prompt: 'SQL Injection 취약점 수정'
|
|
820
|
+
})
|
|
821
|
+
Task({
|
|
822
|
+
subagent_type: 'security-reviewer',
|
|
823
|
+
model: 'opus',
|
|
824
|
+
prompt: '유사 보안 취약점 전체 스캔'
|
|
825
|
+
})
|
|
826
|
+
|
|
827
|
+
// → 수정과 동시에 다른 보안 취약점 발견
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
#### 예시 6: 버그 수정 후 테스트 검증
|
|
831
|
+
|
|
832
|
+
**상황:** 인증 로직 수정 후 실제 서비스 테스트 필요
|
|
833
|
+
|
|
834
|
+
```typescript
|
|
835
|
+
// ✅ 버그 수정 후 테스트 검증
|
|
836
|
+
Task({
|
|
837
|
+
subagent_type: 'implementation-executor',
|
|
838
|
+
model: 'sonnet',
|
|
839
|
+
prompt: '인증 버그 수정: 토큰 재발급 로직 개선'
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
// 수정 완료 후 테스트
|
|
843
|
+
Task({
|
|
844
|
+
subagent_type: 'qa-tester',
|
|
845
|
+
model: 'sonnet',
|
|
846
|
+
prompt: 'tmux 세션으로 수정된 기능 테스트: 로그인 → 토큰 만료 → 재발급 시나리오'
|
|
847
|
+
})
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
#### 예시 7: 라이브러리 버그 조사
|
|
851
|
+
|
|
852
|
+
**상황:** 외부 라이브러리 버전 업그레이드 후 오류 발생
|
|
853
|
+
|
|
854
|
+
```typescript
|
|
855
|
+
// ✅ 라이브러리 버그 조사 + 수정 병렬
|
|
856
|
+
Task({
|
|
857
|
+
subagent_type: 'researcher',
|
|
858
|
+
model: 'sonnet',
|
|
859
|
+
prompt: 'TanStack Query v5.60.0 breaking changes 조사 및 마이그레이션 가이드 탐색'
|
|
860
|
+
})
|
|
861
|
+
Task({
|
|
862
|
+
subagent_type: 'explore',
|
|
863
|
+
model: 'haiku',
|
|
864
|
+
prompt: '현재 코드베이스에서 TanStack Query 사용 위치 전체 탐색'
|
|
865
|
+
})
|
|
866
|
+
|
|
867
|
+
// → 조사 결과 기반으로 수정
|
|
868
|
+
Task({
|
|
869
|
+
subagent_type: 'implementation-executor',
|
|
870
|
+
model: 'sonnet',
|
|
871
|
+
prompt: '[조사 결과 기반] Breaking changes 대응 코드 수정'
|
|
872
|
+
})
|
|
873
|
+
```
|
|
874
|
+
|
|
806
875
|
### Bug Fix Workflow with Agents
|
|
807
876
|
|
|
808
877
|
| 단계 | 작업 | 에이전트 | 모델 | 병렬 실행 |
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: crawler
|
|
3
|
+
description: Playwriter로 웹사이트 직접 탐방하여 크롤링 설계. API/쿠키/토큰/헤더 분석 후 문서화.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Crawler Skill
|
|
8
|
+
|
|
9
|
+
> Playwriter 탐방 → API/Network 분석 → 문서화 → 코드 생성
|
|
10
|
+
|
|
11
|
+
**Templates:** [document-templates.md](references/document-templates.md) · [code-templates.md](references/code-templates.md)
|
|
12
|
+
**Checklists:** [pre-crawl-checklist.md](references/pre-crawl-checklist.md) · [anti-bot-checklist.md](references/anti-bot-checklist.md)
|
|
13
|
+
**References:** [playwriter-commands.md](references/playwriter-commands.md) · [crawling-patterns.md](references/crawling-patterns.md) · [selector-strategies.md](references/selector-strategies.md) · [network-crawling.md](references/network-crawling.md)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<trigger_conditions>
|
|
18
|
+
|
|
19
|
+
| 트리거 | 반응 |
|
|
20
|
+
|--------|------|
|
|
21
|
+
| 크롤링, 스크래핑, crawl, scrape | 즉시 실행 |
|
|
22
|
+
| 웹사이트 데이터 추출 | 즉시 실행 |
|
|
23
|
+
| API 리버스 엔지니어링 | API 인터셉트 |
|
|
24
|
+
| 봇 탐지 우회 | Anti-Detect 참고 |
|
|
25
|
+
|
|
26
|
+
</trigger_conditions>
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
<workflow>
|
|
31
|
+
|
|
32
|
+
| Phase | 작업 | 명령어 |
|
|
33
|
+
|-------|------|--------|
|
|
34
|
+
| **1. 세션** | 생성 + 페이지 열기 | `playwriter session new` |
|
|
35
|
+
| **2. 탐색** | 구조 파악 | `accessibilitySnapshot`, `screenshotWithAccessibilityLabels` |
|
|
36
|
+
| **3. 분석** | API 인터셉트, Selector 추출 | `page.on('response')`, `getLocatorStringForElement` |
|
|
37
|
+
| **4. 문서화** | `.claude/crawler/[사이트]/` 저장 | Write |
|
|
38
|
+
| **5. 코드** | 크롤러 생성 | [code-templates.md](references/code-templates.md) |
|
|
39
|
+
|
|
40
|
+
</workflow>
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
<quick_commands>
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# 세션 생성 + 페이지 열기
|
|
48
|
+
playwriter session new
|
|
49
|
+
playwriter -s 1 -e "state.page = await context.newPage(); await state.page.goto('https://target.com')"
|
|
50
|
+
|
|
51
|
+
# 구조 파악
|
|
52
|
+
playwriter -s 1 -e "console.log(await accessibilitySnapshot({ page: state.page }))"
|
|
53
|
+
|
|
54
|
+
# API 인터셉트
|
|
55
|
+
playwriter -s 1 -e $'
|
|
56
|
+
state.responses = [];
|
|
57
|
+
state.page.on("response", async res => {
|
|
58
|
+
if (res.url().includes("/api/")) {
|
|
59
|
+
try { state.responses.push({ url: res.url(), body: await res.json() }); } catch {}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
'
|
|
63
|
+
|
|
64
|
+
# 인증 추출
|
|
65
|
+
playwriter -s 1 -e "console.log(JSON.stringify(await context.cookies(), null, 2))"
|
|
66
|
+
playwriter -s 1 -e "console.log(await state.page.evaluate(() => localStorage.getItem('token')))"
|
|
67
|
+
|
|
68
|
+
# Selector 변환
|
|
69
|
+
playwriter -s 1 -e "console.log(await getLocatorStringForElement(state.page.locator('aria-ref=e14')))"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
</quick_commands>
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
<method_selection>
|
|
77
|
+
|
|
78
|
+
| 조건 | 방식 | 비고 |
|
|
79
|
+
|------|------|------|
|
|
80
|
+
| API 발견 + 인증 단순 | **fetch** | 가장 빠름 |
|
|
81
|
+
| API + 쿠키/토큰 필요 | **fetch + Cookie** | 만료 관리 필요 |
|
|
82
|
+
| 봇 탐지 강함 | **Nstbrowser** | Anti-Detect |
|
|
83
|
+
| API 없음 (SSR) | **Playwright DOM** | 직접 파싱 |
|
|
84
|
+
|
|
85
|
+
</method_selection>
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
<output_structure>
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
.claude/crawler/[사이트명]/
|
|
93
|
+
├── ANALYSIS.md # 사이트 구조
|
|
94
|
+
├── SELECTORS.md # DOM selector
|
|
95
|
+
├── API.md # API endpoint
|
|
96
|
+
├── NETWORK.md # 인증 정보
|
|
97
|
+
└── CRAWLER.ts # 생성 코드
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Templates:** [document-templates.md](references/document-templates.md)
|
|
101
|
+
|
|
102
|
+
</output_structure>
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
<validation>
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
✅ playwriter 세션 생성
|
|
110
|
+
✅ accessibilitySnapshot 구조 파악
|
|
111
|
+
✅ API 인터셉트 시도
|
|
112
|
+
✅ selector 추출 검증
|
|
113
|
+
✅ .claude/crawler/ 문서화
|
|
114
|
+
✅ 크롤러 코드 생성
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
</validation>
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
<forbidden>
|
|
122
|
+
|
|
123
|
+
| 분류 | 금지 |
|
|
124
|
+
|------|------|
|
|
125
|
+
| **분석** | 구조 파악 없이 selector 추측 |
|
|
126
|
+
| **방식** | API 확인 없이 DOM만 시도 |
|
|
127
|
+
| **문서** | 분석 결과 문서화 생략 |
|
|
128
|
+
| **네트워크** | Rate limiting 미고려 |
|
|
129
|
+
|
|
130
|
+
</forbidden>
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
<example>
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# 사용자: /crawler https://shop.example.com 상품 크롤링
|
|
138
|
+
|
|
139
|
+
# 1. 세션
|
|
140
|
+
playwriter session new # => 1
|
|
141
|
+
playwriter -s 1 -e "state.page = await context.newPage(); await state.page.goto('https://shop.example.com/products')"
|
|
142
|
+
|
|
143
|
+
# 2. 구조 파악
|
|
144
|
+
playwriter -s 1 -e "console.log(await accessibilitySnapshot({ page: state.page }))"
|
|
145
|
+
# => list "Products" [ref=e5]: listitem [ref=e6]: link "Product A" [ref=e7]
|
|
146
|
+
|
|
147
|
+
# 3. API 확인 (스크롤 트리거)
|
|
148
|
+
playwriter -s 1 -e "await state.page.evaluate(() => window.scrollTo(0, 9999))"
|
|
149
|
+
playwriter -s 1 -e "console.log(state.responses.map(r => r.url))"
|
|
150
|
+
# => ["/api/products?page=2"]
|
|
151
|
+
|
|
152
|
+
# 4. 문서화 → .claude/crawler/shop-example-com/
|
|
153
|
+
# 5. API 기반 크롤러 생성
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
</example>
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# 봇 탐지 대응 체크리스트
|
|
2
|
+
|
|
3
|
+
> 크롤러 코드 작성 시 봇 탐지 회피 참고
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<fingerprint>
|
|
8
|
+
|
|
9
|
+
## 브라우저 지문
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
playwriter -s 1 -e $'
|
|
13
|
+
const fp = await state.page.evaluate(() => ({
|
|
14
|
+
webdriver: navigator.webdriver,
|
|
15
|
+
plugins: navigator.plugins.length,
|
|
16
|
+
languages: navigator.languages,
|
|
17
|
+
platform: navigator.platform,
|
|
18
|
+
}));
|
|
19
|
+
console.log(fp);
|
|
20
|
+
'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
| 지문 | 봇 특징 | 대응 |
|
|
24
|
+
|------|--------|------|
|
|
25
|
+
| `navigator.webdriver` | `true` | Anti-Detect |
|
|
26
|
+
| `plugins.length` | `0` | 스푸핑 |
|
|
27
|
+
| UA vs platform | 불일치 | 일관성 |
|
|
28
|
+
| Canvas/WebGL | 일정함 | 다양화 |
|
|
29
|
+
| TLS/JA3 | 비표준 | Anti-Detect |
|
|
30
|
+
|
|
31
|
+
</fingerprint>
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
<behavior>
|
|
36
|
+
|
|
37
|
+
## 행동 패턴
|
|
38
|
+
|
|
39
|
+
| 패턴 | 봇 | 인간 |
|
|
40
|
+
|------|-----|------|
|
|
41
|
+
| 요청 간격 | 일정 | 불규칙 |
|
|
42
|
+
| 클릭 | 즉시 | 호버 후 |
|
|
43
|
+
| 스크롤 | 점프 | 부드럽게 |
|
|
44
|
+
| 체류 시간 | 짧음 | 다양 |
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# 자연스러운 클릭
|
|
48
|
+
playwriter -s 1 -e $'
|
|
49
|
+
const btn = state.page.locator("button");
|
|
50
|
+
await btn.hover();
|
|
51
|
+
await state.page.waitForTimeout(100 + Math.random() * 200);
|
|
52
|
+
await btn.click();
|
|
53
|
+
'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
</behavior>
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
<network>
|
|
61
|
+
|
|
62
|
+
## 네트워크
|
|
63
|
+
|
|
64
|
+
| IP 유형 | 위험도 |
|
|
65
|
+
|---------|-------|
|
|
66
|
+
| 데이터센터 (AWS, GCP) | 높음 |
|
|
67
|
+
| VPN/프록시 | 중간 |
|
|
68
|
+
| 주거용 | 낮음 |
|
|
69
|
+
|
|
70
|
+
**헤더 체크:**
|
|
71
|
+
- `Accept-Language` 지역 일치
|
|
72
|
+
- `Referer` 적절히 설정
|
|
73
|
+
- 헤더 순서 브라우저와 일치
|
|
74
|
+
|
|
75
|
+
</network>
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
<captcha>
|
|
80
|
+
|
|
81
|
+
## CAPTCHA 대응
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
playwriter -s 1 -e $'
|
|
85
|
+
const captcha = await state.page.evaluate(() => ({
|
|
86
|
+
recaptcha: !!document.querySelector(".g-recaptcha"),
|
|
87
|
+
hcaptcha: !!document.querySelector(".h-captcha"),
|
|
88
|
+
turnstile: !!document.querySelector(".cf-turnstile"),
|
|
89
|
+
}));
|
|
90
|
+
console.log(captcha);
|
|
91
|
+
'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
| CAPTCHA | 대응 |
|
|
95
|
+
|---------|------|
|
|
96
|
+
| reCAPTCHA v2 | 2captcha 서비스 |
|
|
97
|
+
| reCAPTCHA v3 | 행동 개선 |
|
|
98
|
+
| hCaptcha | 서비스 사용 |
|
|
99
|
+
| Turnstile | Anti-Detect |
|
|
100
|
+
|
|
101
|
+
</captcha>
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
<test_sites>
|
|
106
|
+
|
|
107
|
+
## 탐지 테스트
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
playwriter -s 1 -e $'
|
|
111
|
+
await state.page.goto("https://bot.sannysoft.com/");
|
|
112
|
+
await state.page.screenshot({ path: "bot-test.png", scale: "css", fullPage: true });
|
|
113
|
+
'
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
| 사이트 | 확인 |
|
|
117
|
+
|--------|------|
|
|
118
|
+
| bot.sannysoft.com | 종합 봇 탐지 |
|
|
119
|
+
| browserleaks.com | 브라우저 지문 |
|
|
120
|
+
| pixelscan.net | Anti-Detect 효과 |
|
|
121
|
+
|
|
122
|
+
</test_sites>
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
<checklist>
|
|
127
|
+
|
|
128
|
+
## 회피 체크리스트
|
|
129
|
+
|
|
130
|
+
**필수:**
|
|
131
|
+
```text
|
|
132
|
+
✅ User-Agent 실제 브라우저
|
|
133
|
+
✅ webdriver = false
|
|
134
|
+
✅ 플러그인/언어/플랫폼 일관성
|
|
135
|
+
✅ 쿠키 활성화
|
|
136
|
+
✅ 헤더 순서 일치
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**행동:**
|
|
140
|
+
```text
|
|
141
|
+
✅ 무작위 딜레이 (1-5초)
|
|
142
|
+
✅ 클릭 전 호버
|
|
143
|
+
✅ 자연스러운 스크롤
|
|
144
|
+
✅ 체류 시간 다양화
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
</checklist>
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
<tool_selection>
|
|
152
|
+
|
|
153
|
+
## 도구 선택
|
|
154
|
+
|
|
155
|
+
| 조건 | 도구 |
|
|
156
|
+
|------|------|
|
|
157
|
+
| 봇 탐지 없음 | Playwright |
|
|
158
|
+
| 기본 탐지 | Playwright + Stealth |
|
|
159
|
+
| 고급 탐지 | Nstbrowser |
|
|
160
|
+
| Cloudflare | Anti-Detect 필수 |
|
|
161
|
+
|
|
162
|
+
</tool_selection>
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# 크롤러 코드 템플릿
|
|
2
|
+
|
|
3
|
+
> 분석 결과 기반 자동 생성용
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 방식 선택
|
|
8
|
+
|
|
9
|
+
| 조건 | 방식 | 템플릿 |
|
|
10
|
+
|------|------|--------|
|
|
11
|
+
| API 발견 + 인증 단순 | fetch | API 크롤러 |
|
|
12
|
+
| API + 쿠키/토큰 | fetch + Cookie | API 크롤러 (인증) |
|
|
13
|
+
| 봇 탐지 강함 | Nstbrowser | (별도 구현) |
|
|
14
|
+
| API 없음 (SSR) | Playwright | DOM 크롤러 |
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## API 크롤러
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
// CRAWLER.ts - API 기반
|
|
22
|
+
interface ApiResponse {
|
|
23
|
+
data: Item[];
|
|
24
|
+
pagination: { page: number; total: number; hasNext: boolean };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface Item {
|
|
28
|
+
id: string;
|
|
29
|
+
title: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ApiCrawler {
|
|
33
|
+
private baseUrl = 'https://example.com/api';
|
|
34
|
+
private headers: Record<string, string> = {
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
// 'Authorization': 'Bearer ...',
|
|
37
|
+
// 'Cookie': '...',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
async fetchPage(page: number, limit = 20): Promise<ApiResponse> {
|
|
41
|
+
const res = await fetch(
|
|
42
|
+
`${this.baseUrl}/items?page=${page}&limit=${limit}`,
|
|
43
|
+
{ headers: this.headers }
|
|
44
|
+
);
|
|
45
|
+
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
|
46
|
+
return res.json();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async crawlAll(): Promise<Item[]> {
|
|
50
|
+
const items: Item[] = [];
|
|
51
|
+
let page = 1;
|
|
52
|
+
let hasNext = true;
|
|
53
|
+
|
|
54
|
+
while (hasNext) {
|
|
55
|
+
const res = await this.fetchPage(page);
|
|
56
|
+
items.push(...res.data);
|
|
57
|
+
hasNext = res.pagination.hasNext;
|
|
58
|
+
page++;
|
|
59
|
+
await new Promise(r => setTimeout(r, 100)); // Rate limit
|
|
60
|
+
}
|
|
61
|
+
return items;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## DOM 크롤러 (Playwright)
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// CRAWLER.ts - DOM 기반
|
|
72
|
+
import { chromium, Browser, Page } from 'playwright';
|
|
73
|
+
|
|
74
|
+
interface Item {
|
|
75
|
+
id: string;
|
|
76
|
+
title: string;
|
|
77
|
+
url: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class DomCrawler {
|
|
81
|
+
private browser: Browser | null = null;
|
|
82
|
+
private page: Page | null = null;
|
|
83
|
+
|
|
84
|
+
async init(): Promise<void> {
|
|
85
|
+
this.browser = await chromium.launch({ headless: true });
|
|
86
|
+
this.page = await this.browser.newPage();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async crawlList(url: string): Promise<Item[]> {
|
|
90
|
+
if (!this.page) throw new Error('Not initialized');
|
|
91
|
+
await this.page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
92
|
+
|
|
93
|
+
return this.page.$$eval('.item-card', cards =>
|
|
94
|
+
cards.map(card => ({
|
|
95
|
+
id: card.getAttribute('data-id') || '',
|
|
96
|
+
title: card.querySelector('h2')?.textContent?.trim() || '',
|
|
97
|
+
url: card.querySelector('a')?.href || '',
|
|
98
|
+
}))
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async crawlAllPages(baseUrl: string): Promise<Item[]> {
|
|
103
|
+
const items: Item[] = [];
|
|
104
|
+
let page = 1;
|
|
105
|
+
|
|
106
|
+
while (true) {
|
|
107
|
+
const result = await this.crawlList(`${baseUrl}?page=${page}`);
|
|
108
|
+
if (result.length === 0) break;
|
|
109
|
+
items.push(...result);
|
|
110
|
+
page++;
|
|
111
|
+
}
|
|
112
|
+
return items;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async close(): Promise<void> {
|
|
116
|
+
await this.browser?.close();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# 크롤링 패턴
|
|
2
|
+
|
|
3
|
+
> 데이터 로딩, 페이지네이션, 인증 패턴
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<rendering>
|
|
8
|
+
|
|
9
|
+
## 렌더링 방식
|
|
10
|
+
|
|
11
|
+
| 방식 | 특징 | 전략 |
|
|
12
|
+
|------|------|------|
|
|
13
|
+
| **SSR** | HTML에 데이터 포함 | DOM 파싱 |
|
|
14
|
+
| **CSR** | JS로 API 호출 | API 직접 호출 |
|
|
15
|
+
|
|
16
|
+
```javascript
|
|
17
|
+
// SSR/CSR 판별
|
|
18
|
+
const html = await page.content();
|
|
19
|
+
console.log(html.length > 5000 ? 'SSR 가능' : 'CSR');
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
</rendering>
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
<pagination>
|
|
27
|
+
|
|
28
|
+
## 페이지네이션
|
|
29
|
+
|
|
30
|
+
### URL 기반
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
for (let page = 1; ; page++) {
|
|
34
|
+
const items = await fetch(`${baseUrl}?page=${page}`).then(r => r.json());
|
|
35
|
+
if (items.length === 0) break;
|
|
36
|
+
allItems.push(...items);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 커서 기반
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
let cursor: string | null = null;
|
|
44
|
+
do {
|
|
45
|
+
const data = await fetch(cursor ? `${url}?cursor=${cursor}` : url).then(r => r.json());
|
|
46
|
+
allItems.push(...data.items);
|
|
47
|
+
cursor = data.nextCursor;
|
|
48
|
+
} while (cursor);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 무한 스크롤
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
// API 인터셉트 후 스크롤
|
|
55
|
+
for (let i = 0; i < 10; i++) {
|
|
56
|
+
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
|
57
|
+
await page.waitForTimeout(1000);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 더보기 버튼
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
const btn = page.locator('button:has-text("더보기")');
|
|
65
|
+
while (await btn.isVisible()) {
|
|
66
|
+
await btn.click();
|
|
67
|
+
await page.waitForLoadState('networkidle');
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
</pagination>
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
<auth>
|
|
76
|
+
|
|
77
|
+
## 인증 패턴
|
|
78
|
+
|
|
79
|
+
### 쿠키/세션
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
const cookies = await context.cookies();
|
|
83
|
+
const session = cookies.find(c => c.name === 'session');
|
|
84
|
+
|
|
85
|
+
await fetch(url, {
|
|
86
|
+
headers: { 'Cookie': `session=${session.value}` }
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Bearer 토큰
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
const token = await page.evaluate(() => localStorage.getItem('token'));
|
|
94
|
+
|
|
95
|
+
await fetch(url, {
|
|
96
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
</auth>
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
<dynamic>
|
|
105
|
+
|
|
106
|
+
## 동적 콘텐츠
|
|
107
|
+
|
|
108
|
+
### Lazy Loading
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
// 스크롤로 이미지 로드
|
|
112
|
+
await page.evaluate(async () => {
|
|
113
|
+
for (let y = 0; y < document.body.scrollHeight; y += 500) {
|
|
114
|
+
window.scrollTo(0, y);
|
|
115
|
+
await new Promise(r => setTimeout(r, 200));
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Shadow DOM
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
const content = await page.evaluate(() => {
|
|
124
|
+
return document.querySelector('custom-element').shadowRoot.innerHTML;
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### iframe
|
|
129
|
+
|
|
130
|
+
```javascript
|
|
131
|
+
const frame = page.frameLocator('#content-frame');
|
|
132
|
+
const items = await frame.locator('.item').allTextContents();
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
</dynamic>
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
<rate_limit>
|
|
140
|
+
|
|
141
|
+
## Rate Limiting
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// 딜레이 적용
|
|
145
|
+
async function rateLimitedFetch(urls: string[], delayMs = 100) {
|
|
146
|
+
const results = [];
|
|
147
|
+
for (const url of urls) {
|
|
148
|
+
results.push(await fetch(url).then(r => r.json()));
|
|
149
|
+
await new Promise(r => setTimeout(r, delayMs));
|
|
150
|
+
}
|
|
151
|
+
return results;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 재시도
|
|
155
|
+
async function fetchWithRetry(url: string, retries = 3) {
|
|
156
|
+
for (let i = 0; i < retries; i++) {
|
|
157
|
+
const res = await fetch(url);
|
|
158
|
+
if (res.status === 429) {
|
|
159
|
+
await new Promise(r => setTimeout(r, (parseInt(res.headers.get('Retry-After') || '60')) * 1000));
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
return res.json();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
</rate_limit>
|