@su-record/vibe 2.8.35 → 2.8.37
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/README.md +20 -8
- package/dist/infra/lib/browser/capture.d.ts +26 -0
- package/dist/infra/lib/browser/capture.d.ts.map +1 -0
- package/dist/infra/lib/browser/capture.js +115 -0
- package/dist/infra/lib/browser/capture.js.map +1 -0
- package/dist/infra/lib/browser/compare.d.ts +19 -0
- package/dist/infra/lib/browser/compare.d.ts.map +1 -0
- package/dist/infra/lib/browser/compare.js +133 -0
- package/dist/infra/lib/browser/compare.js.map +1 -0
- package/dist/infra/lib/browser/index.d.ts +5 -0
- package/dist/infra/lib/browser/index.d.ts.map +1 -0
- package/dist/infra/lib/browser/index.js +4 -0
- package/dist/infra/lib/browser/index.js.map +1 -0
- package/dist/infra/lib/browser/launch.d.ts +19 -0
- package/dist/infra/lib/browser/launch.d.ts.map +1 -0
- package/dist/infra/lib/browser/launch.js +59 -0
- package/dist/infra/lib/browser/launch.js.map +1 -0
- package/dist/infra/lib/browser/types.d.ts +103 -0
- package/dist/infra/lib/browser/types.d.ts.map +1 -0
- package/dist/infra/lib/browser/types.js +8 -0
- package/dist/infra/lib/browser/types.js.map +1 -0
- package/hooks/scripts/llm-orchestrate.js +4 -6
- package/package.json +2 -2
- package/skills/vibe.figma/SKILL.md +649 -107
- package/skills/vibe.figma.convert/SKILL.md +139 -161
- package/skills/vibe.figma.extract/SKILL.md +86 -40
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vibe.figma
|
|
3
|
-
description: Figma design to code —
|
|
3
|
+
description: Figma design to code — 시각 기반 퍼즐 조립 방식
|
|
4
4
|
triggers: []
|
|
5
5
|
tier: standard
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# vibe.figma —
|
|
8
|
+
# vibe.figma — Visual Puzzle Assembly
|
|
9
|
+
|
|
10
|
+
## 핵심 원칙
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
스크린샷이 정답이다. Figma 데이터는 재료일 뿐이다.
|
|
14
|
+
|
|
15
|
+
❌ Figma 트리 구조를 HTML로 변환하지 않는다 (이 방식은 실패한다)
|
|
16
|
+
✅ 스크린샷을 보고 "무엇을 만들어야 하는지" 파악한다
|
|
17
|
+
✅ Figma 데이터(이미지, 색상, 수치)를 정확한 재료로 사용한다
|
|
18
|
+
✅ 사람 개발자처럼: 디자인 보고 → 에셋 받고 → 만들면서 비교
|
|
19
|
+
```
|
|
9
20
|
|
|
10
21
|
## 금지 사항
|
|
11
22
|
|
|
12
23
|
```
|
|
13
|
-
❌
|
|
24
|
+
❌ Figma 레이어 트리를 그대로 div 구조로 변환
|
|
25
|
+
❌ CSS로 이미지 재현 (gradient/shape으로 그림 그리기)
|
|
14
26
|
❌ 이미지 다운로드 없이 코드 생성 진행
|
|
15
|
-
❌ placeholder / 빈
|
|
16
|
-
❌
|
|
17
|
-
❌ 브라우저 기본 스타일(검은색 16px)로 보이는 텍스트
|
|
18
|
-
❌ 핵심 에셋만 다운로드 (const img... 전부 다운로드)
|
|
27
|
+
❌ placeholder / 빈 src="" 남기기
|
|
28
|
+
❌ 색상·크기를 추정 (재료함에 정확한 값이 있음)
|
|
19
29
|
❌ 컴포넌트 파일 안에 <style> 블록 / 인라인 style=""
|
|
20
30
|
✅ 외부 SCSS 파일에만 스타일 작성
|
|
21
31
|
```
|
|
@@ -25,9 +35,10 @@ tier: standard
|
|
|
25
35
|
```
|
|
26
36
|
/vibe.figma
|
|
27
37
|
→ Phase 0: Setup (스택 감지, 디렉토리 생성)
|
|
28
|
-
→ Phase 1: Storyboard (
|
|
29
|
-
→ Phase 2:
|
|
30
|
-
→ Phase 3:
|
|
38
|
+
→ Phase 1: Storyboard (스토리보드 → 레이아웃 + 컴포넌트 + 기능 정의)
|
|
39
|
+
→ Phase 2: 재료 확보 (디자인 URL → 스크린샷 + 이미지 + CSS + 텍스트)
|
|
40
|
+
→ Phase 3: 퍼즐 조립 (스크린샷 보면서 Phase 1 컴포넌트에 디자인 입히기)
|
|
41
|
+
→ Phase 4: 검증 루프 (빌드 → 스크린샷 → 비교 → 수정 → 반복)
|
|
31
42
|
```
|
|
32
43
|
|
|
33
44
|
---
|
|
@@ -52,6 +63,100 @@ tier: standard
|
|
|
52
63
|
- components/{feature}/
|
|
53
64
|
- public/images/{feature}/ (또는 static/images/{feature}/)
|
|
54
65
|
- styles/{feature}/ (layout/, components/ 하위)
|
|
66
|
+
|
|
67
|
+
5. 기존 컴포넌트 스캔 + 인덱싱:
|
|
68
|
+
- Glob "components/**/*.vue" or "components/**/*.tsx"
|
|
69
|
+
- 재사용 가능한 컴포넌트 목록 수집 (GNB, Footer, Button 등)
|
|
70
|
+
|
|
71
|
+
5-1. 컴포넌트 상세 인덱싱 (50개 이하 스캔):
|
|
72
|
+
대상 파일 수집 (우선순위 순서, 합산 50개 이내):
|
|
73
|
+
barrel file index.ts → components/ui/ → components/common/ → components/shared/ → 나머지
|
|
74
|
+
각 디렉토리 내에서는 1-depth 파일만 (하위 재귀 탐색 안 함)
|
|
75
|
+
|
|
76
|
+
각 파일에 대해 2단계 추출:
|
|
77
|
+
a. Grep: defineProps|interface Props|<slot|@description|class=|className=
|
|
78
|
+
→ 시작 줄 번호 탐색
|
|
79
|
+
b. Read: 시작줄 ~ +30줄 추출
|
|
80
|
+
Vue/Svelte template+script 분리 파일: 첫 300줄 전체 Read
|
|
81
|
+
barrel file (index.ts): 전체 Read
|
|
82
|
+
|
|
83
|
+
추출 항목:
|
|
84
|
+
- Props/Interface: defineProps<{...}> 또는 interface Props {...} 에서 prop 이름+타입
|
|
85
|
+
※ 외부 타입 참조 시 (defineProps<ButtonProps>()) → types 인덱스에서 교차 참조
|
|
86
|
+
- Slots: <slot> 또는 {children} 패턴
|
|
87
|
+
- 스타일 클래스: class="..." 또는 className={styles.xxx} 최상위 요소의 클래스명
|
|
88
|
+
- 용도 힌트: JSDoc @description 또는 파일 첫 번째 주석
|
|
89
|
+
|
|
90
|
+
5-2. 인덱스 결과 저장:
|
|
91
|
+
저장 경로: /tmp/{feature}/component-index.json
|
|
92
|
+
[
|
|
93
|
+
{ name: "BaseButton",
|
|
94
|
+
path: "components/common/BaseButton.vue",
|
|
95
|
+
props: [{ name: "label", type: "string" }, { name: "variant", type: "'primary'|'secondary'" }],
|
|
96
|
+
slots: ["default"],
|
|
97
|
+
classes: ["btn", "btn--primary"],
|
|
98
|
+
description: "공통 버튼 컴포넌트" },
|
|
99
|
+
...
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
컨텍스트 관리:
|
|
103
|
+
- 컴포넌트 20개 이하: 전체 인덱스를 프롬프트에 포함
|
|
104
|
+
- 컴포넌트 20개 초과: 이름+설명+축약 props(이름만, 타입 생략)만 포함
|
|
105
|
+
매칭 후보 발견 시 해당 파일 Read로 상세 확인
|
|
106
|
+
- classes 필드는 요약에서 제외 (매칭 시 파일 Read로 확인)
|
|
107
|
+
|
|
108
|
+
5-3. Hooks/Types/Constants 인덱싱:
|
|
109
|
+
추가 스캔 대상:
|
|
110
|
+
- Composables/Hooks: composables/**/*.ts, hooks/**/*.ts
|
|
111
|
+
→ export 함수명 + 파라미터 + 반환 타입
|
|
112
|
+
- 타입 정의: types/**/*.ts, types.ts
|
|
113
|
+
→ export interface/type 이름 + 최상위 필드
|
|
114
|
+
- 상수: constants/**/*.ts
|
|
115
|
+
→ export const 이름 + 값 (또는 타입)
|
|
116
|
+
|
|
117
|
+
저장: /tmp/{feature}/context-index.json (component-index와 별도)
|
|
118
|
+
컨텍스트 관리:
|
|
119
|
+
- 항목 30개 이하: 전체 인덱스를 프롬프트에 포함
|
|
120
|
+
- 항목 30개 초과: 이름+핵심 시그니처만 요약, 상세 필요 시 파일 Read로 지연 조회
|
|
121
|
+
|
|
122
|
+
타임아웃: 파일당 Read 최대 300줄, 전체 인덱싱 2분 이내 완료
|
|
123
|
+
|
|
124
|
+
6. 기존 디자인 토큰 스캔:
|
|
125
|
+
|
|
126
|
+
SCSS 토큰:
|
|
127
|
+
Glob: **/_variables.scss, **/_tokens.scss, **/_colors.scss, **/variables.scss
|
|
128
|
+
→ 패턴: $변수명: 값; 추출
|
|
129
|
+
→ 결과: { name: "$color-primary", value: "#3b82f6", source: "styles/_variables.scss" }
|
|
130
|
+
|
|
131
|
+
CSS Variables:
|
|
132
|
+
Glob: **/global.css, **/variables.css, **/root.css, **/app.css
|
|
133
|
+
Grep: "--[a-zA-Z].*:" 패턴
|
|
134
|
+
→ 결과: { name: "--color-primary", value: "#3b82f6", source: "styles/global.css" }
|
|
135
|
+
|
|
136
|
+
Tailwind:
|
|
137
|
+
tailwind.config.{js,ts,mjs} 존재 시 Read
|
|
138
|
+
→ theme.colors, theme.extend.colors 에서 커스텀 색상 추출
|
|
139
|
+
→ theme.spacing, theme.fontSize 에서 커스텀 값 추출
|
|
140
|
+
→ Tailwind v4: global CSS에서 @theme 디렉티브 내 CSS variable도 스캔
|
|
141
|
+
→ 결과: { name: "blue-500", value: "#3b82f6", type: "tailwind", category: "color" }
|
|
142
|
+
|
|
143
|
+
CSS-in-JS:
|
|
144
|
+
Glob: **/theme.{ts,js}, **/tokens.{ts,js}, **/design-tokens.{ts,js}
|
|
145
|
+
→ export 객체에서 color/spacing/typography 키 추출
|
|
146
|
+
|
|
147
|
+
통합 결과 → /tmp/{feature}/project-tokens.json:
|
|
148
|
+
{
|
|
149
|
+
colors: [{ name, value, source }],
|
|
150
|
+
spacing: [{ name, value, source }],
|
|
151
|
+
typography: [{ name, value, source }],
|
|
152
|
+
other: [{ name, value, source }]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
토큰 소스 우선순위 (복수 시스템 공존 시):
|
|
156
|
+
Tailwind > CSS Variables > SCSS > CSS-in-JS
|
|
157
|
+
|
|
158
|
+
성능: 100개 파일 기준 5초 이내 완료
|
|
159
|
+
파싱 실패 시: 해당 파일 스킵 + 경고 로그 (전체 중단하지 않음)
|
|
55
160
|
```
|
|
56
161
|
|
|
57
162
|
---
|
|
@@ -86,7 +191,7 @@ URL에서 fileKey, nodeId 추출
|
|
|
86
191
|
1순위: SPEC (기능 정의서) — 1개
|
|
87
192
|
2순위: CONFIG (해상도) — 1개
|
|
88
193
|
3순위: PAGE 중 메인 섹션만 (3.1, 3.2, 3.3, 3.4, 3.5, 3.6)
|
|
89
|
-
하위 케이스(3.1.1, 3.2.1 등)는 건너뜀
|
|
194
|
+
하위 케이스(3.1.1, 3.2.1 등)는 건너뜀
|
|
90
195
|
4순위: SHARED (공통 요소, Popup) — 필요 시
|
|
91
196
|
|
|
92
197
|
높이 1500px 이상 프레임:
|
|
@@ -139,7 +244,7 @@ URL에서 fileKey, nodeId 추출
|
|
|
139
244
|
- 목 데이터 (빈 배열 금지, 3~7개 아이템)
|
|
140
245
|
- 이벤트 핸들러 stub (body는 // TODO:)
|
|
141
246
|
|
|
142
|
-
<style> 블록 없음 — 스타일은 Phase
|
|
247
|
+
<style> 블록 없음 — 스타일은 Phase 3에서 외부 파일로.
|
|
143
248
|
|
|
144
249
|
3. 공통 컴포넌트 (SHARED에서 파악):
|
|
145
250
|
→ 프로젝트에 이미 있으면 import 재사용
|
|
@@ -165,7 +270,112 @@ Phase 1 완료 조건:
|
|
|
165
270
|
□ 빌드 성공
|
|
166
271
|
|
|
167
272
|
빈 화면 = Phase 1 미완성. Phase 2로 넘어가지 않는다.
|
|
168
|
-
스타일/이미지는 없어도 됨 — Phase
|
|
273
|
+
스타일/이미지는 없어도 됨 — Phase 3에서 채움.
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Phase 2: 재료 확보
|
|
279
|
+
|
|
280
|
+
Phase 1 컴포넌트가 준비된 상태에서, 디자인 URL로 시각 재료를 수집한다.
|
|
281
|
+
|
|
282
|
+
사용자에게 질문한다:
|
|
283
|
+
- question: "디자인 Figma URL을 입력해주세요. 여러 페이지는 줄바꿈으로 구분."
|
|
284
|
+
- options 제공 금지 — 자유 텍스트 입력만 허용
|
|
285
|
+
|
|
286
|
+
### 멀티 프레임 URL 검증
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
URL 유효성 검증:
|
|
290
|
+
- 모든 URL에서 fileKey 추출 → 동일한 fileKey인지 확인
|
|
291
|
+
- 서로 다른 fileKey 발견 시 에러: "멀티 프레임은 동일 Figma 파일 내 다른 프레임만 지원합니다"
|
|
292
|
+
- node-id 파라미터 누락 시 해당 URL 에러 보고
|
|
293
|
+
- 최대 5개 URL까지 지원
|
|
294
|
+
|
|
295
|
+
URL 개수에 따른 처리:
|
|
296
|
+
1개: 기존 방식 (변경 없음, 아래 2-1 그대로)
|
|
297
|
+
2개 이상: 멀티 프레임 모드 활성화 (2-1-multi 참조)
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### 2-1. 디자인 재료 추출 (단일 URL)
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
URL에서 fileKey, nodeId 추출
|
|
304
|
+
|
|
305
|
+
1단계 — 전체 스크린샷 (정답 사진):
|
|
306
|
+
node "[FIGMA_SCRIPT]" screenshot {fileKey} {nodeId} --out=/tmp/{feature}/full-screenshot.png
|
|
307
|
+
|
|
308
|
+
2단계 — 전체 트리 + CSS (수치 재료):
|
|
309
|
+
node "[FIGMA_SCRIPT]" tree {fileKey} {nodeId} --depth=10
|
|
310
|
+
→ /tmp/{feature}/tree.json 에 저장
|
|
311
|
+
|
|
312
|
+
3단계 — 전체 이미지 다운로드 (시각 재료):
|
|
313
|
+
node "[FIGMA_SCRIPT]" images {fileKey} {nodeId} --out=/tmp/{feature}/images/ --depth=10
|
|
314
|
+
→ 모든 이미지 에셋 확보. 누락 0건, 0byte 0건.
|
|
315
|
+
|
|
316
|
+
4단계 — 섹션별 스크린샷 (부분 정답):
|
|
317
|
+
트리의 1depth 자식 프레임 각각:
|
|
318
|
+
node "[FIGMA_SCRIPT]" screenshot {fileKey} {child.nodeId} --out=/tmp/{feature}/sections/{child.name}.png
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### 2-1-multi. 멀티 프레임 재료 확보 (2개 이상 URL)
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
각 URL에 대해 순차 추출 (Figma API rate limit: 요청 간 500ms 간격):
|
|
325
|
+
URL 1 → /tmp/{feature}/frame-1/ (full-screenshot, tree, images, sections)
|
|
326
|
+
URL 2 → /tmp/{feature}/frame-2/ (URL 1 완료 후 500ms 대기)
|
|
327
|
+
URL 3 → /tmp/{feature}/frame-3/ (URL 2 완료 후 500ms 대기)
|
|
328
|
+
※ 추출 완료 후 후처리(섹션 분할 등)는 병렬 가능
|
|
329
|
+
|
|
330
|
+
부분 실패 처리:
|
|
331
|
+
- 개별 URL 추출 실패 시 해당 frame만 건너뛰고 나머지 진행
|
|
332
|
+
- 실패한 frame은 사용자에게 즉시 보고: "frame-{N} 추출 실패: {에러 사유}" (API 에러, 권한 부족, 잘못된 nodeId 등)
|
|
333
|
+
- 성공한 frame ≥ 2: Phase 2.5 계속 진행
|
|
334
|
+
- 정확히 1개만 성공: 단일 프레임 모드로 폴백 (Phase 2.5 스킵)
|
|
335
|
+
- 0개 성공: 전체 실패 보고
|
|
336
|
+
|
|
337
|
+
결과:
|
|
338
|
+
/tmp/{feature}/
|
|
339
|
+
├── frame-1/ ← 메인 페이지
|
|
340
|
+
│ ├── full-screenshot.png
|
|
341
|
+
│ ├── tree.json
|
|
342
|
+
│ ├── images/
|
|
343
|
+
│ └── sections/
|
|
344
|
+
├── frame-2/ ← 서브 페이지 1
|
|
345
|
+
│ └── ...
|
|
346
|
+
├── frame-3/ ← 서브 페이지 2
|
|
347
|
+
│ └── ...
|
|
348
|
+
└── shared/ ← 공통 분석 결과 (Phase 2.5에서 생성)
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### 2-2. 재료함 정리
|
|
352
|
+
|
|
353
|
+
```
|
|
354
|
+
Phase 2 완료 시 /tmp/{feature}/ 에 다음이 준비되어야 함:
|
|
355
|
+
|
|
356
|
+
단일 URL:
|
|
357
|
+
/tmp/{feature}/
|
|
358
|
+
├── full-screenshot.png ← 전체 정답 사진
|
|
359
|
+
├── tree.json ← 노드 트리 + CSS 수치
|
|
360
|
+
├── images/ ← 모든 이미지 에셋
|
|
361
|
+
│ ├── hero-bg.png
|
|
362
|
+
│ ├── hero-title.png
|
|
363
|
+
│ ├── card-item-1.png
|
|
364
|
+
│ └── ...
|
|
365
|
+
└── sections/ ← 섹션별 정답 사진
|
|
366
|
+
├── hero.png
|
|
367
|
+
├── daily-checkin.png
|
|
368
|
+
├── playtime-mission.png
|
|
369
|
+
└── ...
|
|
370
|
+
|
|
371
|
+
멀티 URL: 위 2-1-multi 결과 구조 참조
|
|
372
|
+
|
|
373
|
+
재료 목록 (material-inventory):
|
|
374
|
+
- 이미지: 파일명 + 크기 + 용도 추정 (BG/icon/title/decoration)
|
|
375
|
+
- 색상: tree.json에서 추출한 모든 고유 색상값
|
|
376
|
+
- 폰트: 사용된 font-family, size, weight 목록
|
|
377
|
+
- 텍스트: 모든 TEXT 노드의 characters 값
|
|
378
|
+
- 간격: padding, gap, margin 사용 빈도 높은 값
|
|
169
379
|
```
|
|
170
380
|
|
|
171
381
|
### 스케일 팩터
|
|
@@ -181,145 +391,477 @@ Phase 1 완료 조건:
|
|
|
181
391
|
|
|
182
392
|
---
|
|
183
393
|
|
|
184
|
-
## Phase 2:
|
|
394
|
+
## Phase 2.5: 공통 패턴 분석 (멀티 프레임 전용)
|
|
185
395
|
|
|
186
|
-
|
|
187
|
-
|
|
396
|
+
**URL 2개 이상일 때만 실행. 단일 URL이면 Phase 3으로 건너뜀.**
|
|
397
|
+
**프레임 간 공통 요소를 식별하여 공유 컴포넌트로 추출한다.**
|
|
188
398
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
-
|
|
399
|
+
```
|
|
400
|
+
1. 시각 비교 — 각 프레임의 스크린샷을 순차 Read:
|
|
401
|
+
- frame-1/full-screenshot.png → frame-2/full-screenshot.png → ...
|
|
402
|
+
- 시각적으로 반복되는 요소 식별:
|
|
403
|
+
상단 영역 (GNB/Header), 하단 영역 (Footer),
|
|
404
|
+
카드 패턴, 버튼 스타일, 섹션 레이아웃
|
|
405
|
+
|
|
406
|
+
2. 구조 비교 — 각 tree.json의 1depth 자식 비교:
|
|
407
|
+
- 동일한 name 또는 prefix 일치 (예: "GNB", "Header", "Footer", "Nav")
|
|
408
|
+
- 동일한 size (width와 height 모두 ±10% 이내): 같은 컴포넌트 후보
|
|
409
|
+
- 동일한 CSS 패턴: 같은 스타일 사용 (색상, 폰트, 레이아웃)
|
|
410
|
+
|
|
411
|
+
3. 공통 컴포넌트 후보 목록:
|
|
412
|
+
shared-components:
|
|
413
|
+
- name: GNB
|
|
414
|
+
frames: [frame-1, frame-2, frame-3]
|
|
415
|
+
consistency: 100% (모든 프레임에서 동일)
|
|
416
|
+
action: 공유 컴포넌트로 1회만 생성
|
|
417
|
+
- name: Footer
|
|
418
|
+
frames: [frame-1, frame-3]
|
|
419
|
+
consistency: 67% (2/3 프레임)
|
|
420
|
+
action: 공유 컴포넌트 + frame-2는 다른 Footer
|
|
421
|
+
- name: CardItem
|
|
422
|
+
frames: [frame-1, frame-2]
|
|
423
|
+
size-match: 95%
|
|
424
|
+
action: 공유 컴포넌트 (props로 변형)
|
|
425
|
+
|
|
426
|
+
4. 공통 토큰 추출:
|
|
427
|
+
- 모든 프레임의 색상 팔레트 합집합 → 공유 _tokens.scss
|
|
428
|
+
- 모든 프레임의 폰트 목록 합집합
|
|
429
|
+
- 모든 프레임의 간격 패턴 합집합
|
|
430
|
+
→ 프레임별 고유 값만 프레임 로컬 토큰으로 분리
|
|
431
|
+
```
|
|
192
432
|
|
|
193
|
-
|
|
433
|
+
---
|
|
194
434
|
|
|
195
|
-
|
|
196
|
-
- question: "다음 브레이크포인트 디자인 URL을 입력해주세요. (없으면 '없음')"
|
|
197
|
-
→ URL 입력 시: 2-3 반응형 추가 후 다시 질문
|
|
198
|
-
→ "없음" 응답 시: Phase 3으로
|
|
435
|
+
## Phase 3: 퍼즐 조립
|
|
199
436
|
|
|
200
|
-
|
|
437
|
+
**Phase 1에서 만든 컴포넌트에 Phase 2의 재료로 디자인을 입힌다.**
|
|
438
|
+
**스크린샷을 보면서 퍼즐을 맞추듯 조립한다.**
|
|
439
|
+
**첫 섹션(Hero) 단독 완료 후 나머지 섹션 병렬 진행.**
|
|
201
440
|
|
|
441
|
+
**멀티 프레임 모드 시 조립 순서 변경:**
|
|
442
|
+
```
|
|
443
|
+
멀티 프레임 Phase 3 순서:
|
|
444
|
+
|
|
445
|
+
1단계: 공유 컴포넌트 먼저 생성
|
|
446
|
+
- shared-components 목록의 컴포넌트를 components/shared/에 생성
|
|
447
|
+
- 가장 일관적인 프레임(shared-components 매칭 수가 가장 많은 프레임) 기준으로 조립
|
|
448
|
+
- 프레임별 변형은 props로 처리
|
|
449
|
+
|
|
450
|
+
2단계: 프레임별 고유 섹션 조립
|
|
451
|
+
- frame-1 고유 섹션: 공유 컴포넌트 import + 고유 섹션 신규 생성
|
|
452
|
+
- frame-2 고유 섹션: 동일
|
|
453
|
+
- 각 프레임의 페이지 파일에서 공유 + 고유 컴포넌트 배치
|
|
454
|
+
|
|
455
|
+
3단계: 스타일 구조
|
|
456
|
+
styles/{feature}/
|
|
457
|
+
├── _tokens.scss ← 공유 토큰 (합집합)
|
|
458
|
+
├── shared/ ← 공유 컴포넌트 스타일
|
|
459
|
+
│ ├── layout/_gnb.scss
|
|
460
|
+
│ └── components/_gnb.scss
|
|
461
|
+
├── frame-1/ ← 메인 고유 스타일
|
|
462
|
+
│ ├── layout/
|
|
463
|
+
│ └── components/
|
|
464
|
+
└── frame-2/
|
|
465
|
+
|
|
466
|
+
단일 URL: 기존 방식 그대로 (위 멀티 프레임 순서 무시)
|
|
202
467
|
```
|
|
203
|
-
|
|
468
|
+
|
|
469
|
+
### 3-0. SCSS Setup + 등록 (첫 섹션 전)
|
|
470
|
+
|
|
471
|
+
```
|
|
472
|
+
Phase 1에서 생성한 빈 SCSS 파일에 기본 내용 Write:
|
|
204
473
|
styles/{feature}/index.scss ← @import 진입점
|
|
205
|
-
styles/{feature}/_tokens.scss ←
|
|
474
|
+
styles/{feature}/_tokens.scss ← 재료함에서 추출한 디자인 토큰
|
|
206
475
|
styles/{feature}/_mixins.scss ← breakpoint mixin
|
|
207
|
-
styles/{feature}/_base.scss ← 루트 클래스
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
476
|
+
styles/{feature}/_base.scss ← 루트 클래스
|
|
477
|
+
|
|
478
|
+
토큰 매핑 (기존 토큰 우선 사용):
|
|
479
|
+
1. /tmp/{feature}/project-tokens.json 을 Read로 로드
|
|
480
|
+
2. Figma 재료함의 각 값에 대해 project-tokens에서 동일 값 검색:
|
|
481
|
+
- 색상: hex 정규화 후 완전 일치 (Figma RGBA 0-1 → hex, 3자리→6자리, 대소문자 무시)
|
|
482
|
+
※ alpha < 1: 8자리 hex (#RRGGBBAA) 또는 rgba() 함수로 변환
|
|
483
|
+
- 간격: px 값 완전 일치 (rem→px: 1rem=16px)
|
|
484
|
+
- 폰트: family 이름 포함 매칭
|
|
485
|
+
3. 매칭됨 → 기존 토큰 참조:
|
|
486
|
+
- SCSS: @use 'path' 로 기존 파일 import, $변수명 사용
|
|
487
|
+
- Tailwind: 해당 유틸리티 클래스 사용 (bg-blue-500)
|
|
488
|
+
- CSS Variables: var(--color-primary) 사용
|
|
489
|
+
4. 매칭 안 됨 → 새 토큰 생성 (기존 방식)
|
|
490
|
+
|
|
491
|
+
_tokens.scss 구조:
|
|
492
|
+
// ─── 기존 토큰 참조 ────────────────────
|
|
493
|
+
@use '../../styles/variables' as v;
|
|
494
|
+
|
|
495
|
+
// ─── 매핑 (기존 토큰 → 피처 시맨틱) ────
|
|
496
|
+
$color-bg-primary: v.$color-navy; // 기존 재사용
|
|
497
|
+
$color-text-primary: v.$color-white; // 기존 재사용
|
|
498
|
+
|
|
499
|
+
// ─── 새 토큰 (매칭 안 된 값만) ─────────
|
|
500
|
+
$color-accent-gold: #ffd700; // 새 값
|
|
501
|
+
$space-section-gap: 42px; // 새 값
|
|
502
|
+
|
|
503
|
+
매핑 결과 보고: "토큰 매핑: 12/18 매칭 (67%), 6개 새 토큰 생성"
|
|
504
|
+
|
|
505
|
+
기존 토큰 파일 수정 금지 — 참조만 함
|
|
506
|
+
같은 값의 토큰 중복 생성 금지
|
|
507
|
+
새 토큰은 피처 스코프 네이밍 ($feature-color-xxx)
|
|
508
|
+
|
|
509
|
+
project-tokens.json 없는 경우 (기존 토큰 없음):
|
|
510
|
+
기존 방식으로 전체 생성:
|
|
511
|
+
- 색상 → primitive ($color-navy: #0a1628) + semantic ($color-bg-primary)
|
|
512
|
+
- 폰트 → $font-pretendard, $font-size-md: 16px
|
|
513
|
+
- 간격 → $space-sm: 8px, $space-md: 16px
|
|
514
|
+
|
|
515
|
+
스타일 등록 (BLOCKING):
|
|
212
516
|
Grep "{feature}/index.scss" → 이미 등록되어 있으면 건너뜀.
|
|
517
|
+
없으면 프로젝트 방식에 맞게 등록.
|
|
518
|
+
```
|
|
213
519
|
|
|
214
|
-
|
|
215
|
-
루트 페이지 파일에서 직접 로드:
|
|
216
|
-
pages/{feature}.vue → <style lang="scss" src="~/assets/scss/{feature}/index.scss" />
|
|
217
|
-
app/{feature}/page.tsx → import '~/styles/{feature}/index.scss'
|
|
520
|
+
### 3-0.5. 컴포넌트 매칭 (각 섹션 조립 전)
|
|
218
521
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
522
|
+
```
|
|
523
|
+
/tmp/{feature}/component-index.json 을 Read로 로드한다.
|
|
524
|
+
|
|
525
|
+
각 섹션 스크린샷 분석 후, 기존 컴포넌트 매칭:
|
|
526
|
+
|
|
527
|
+
1. 스크린샷에서 식별된 UI 요소를 component-index와 대조
|
|
528
|
+
2. 매칭 판정 기준:
|
|
529
|
+
- 필수: name/role 유사성 (시각적 역할 일치)
|
|
530
|
+
버튼 → BaseButton, 네비게이션 → GNB, 카드 → Card, 모달 → Modal
|
|
531
|
+
- 보조: props 호환성 (필요 props가 기존 컴포넌트에 존재)
|
|
532
|
+
- 보조: slots 호환성 (필요 slot이 존재)
|
|
533
|
+
- 불일치: props/slots 50% 미만 호환이면 매칭 거부 → 새로 생성
|
|
534
|
+
3. 매칭된 컴포넌트: import하여 props 전달 (내부 수정 금지)
|
|
535
|
+
4. 매칭 안 됨: 새로 생성 (강제 재사용 금지)
|
|
536
|
+
|
|
537
|
+
재사용 시 스타일 충돌 방지:
|
|
538
|
+
✅ 기존 컴포넌트 import하여 사용
|
|
539
|
+
✅ props로 variant/size 등 커스터마이즈
|
|
540
|
+
✅ 래퍼 클래스로 position/size만 오버라이드
|
|
541
|
+
❌ 기존 컴포넌트 내부 스타일 수정 금지
|
|
542
|
+
❌ 90% 유사한데 새로 만들기 금지
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### 3-1. 섹션 조립 프로세스
|
|
222
546
|
|
|
223
|
-
|
|
547
|
+
```
|
|
548
|
+
각 섹션에 대해:
|
|
549
|
+
|
|
550
|
+
1. 정답 확인 — 섹션 스크린샷을 Read로 본다
|
|
551
|
+
/tmp/{feature}/sections/{section}.png
|
|
552
|
+
→ "이 화면처럼 만들어야 한다"
|
|
553
|
+
|
|
554
|
+
2. 재료 확인 — 이 섹션에 필요한 재료 목록 확인
|
|
555
|
+
- 이미지: /tmp/{feature}/images/ 에서 해당 파일들
|
|
556
|
+
- CSS 수치: tree.json에서 해당 노드의 정확한 값
|
|
557
|
+
- 텍스트: TEXT 노드의 characters
|
|
558
|
+
|
|
559
|
+
3. Phase 1 컴포넌트에 디자인 입히기
|
|
560
|
+
a. 이미지 복사: images/ → static/images/{feature}/
|
|
561
|
+
b. template 업데이트:
|
|
562
|
+
- Phase 1의 기능 요소(v-for, @click, v-if, $emit) 보존
|
|
563
|
+
- 스크린샷을 보고 시각 구조에 맞게 HTML 재구성
|
|
564
|
+
- Figma 레이어 구조를 HTML로 변환하지 않음 (시각 기반)
|
|
565
|
+
- 재료함의 정확한 이미지 경로 사용
|
|
566
|
+
- 재료함의 정확한 텍스트 사용
|
|
567
|
+
c. script 보존:
|
|
568
|
+
- Phase 1의 JSDoc, 인터페이스, 핸들러 유지
|
|
569
|
+
d. SCSS 작성: 재료함의 정확한 CSS 수치 사용 (scaleFactor 적용)
|
|
570
|
+
- layout/_{section}.scss ← 포지션, 사이즈, 플렉스
|
|
571
|
+
- components/_{section}.scss ← 폰트, 색상, 보더
|
|
572
|
+
- _tokens.scss에 새 토큰 추가
|
|
573
|
+
|
|
574
|
+
4. 즉시 검증 — 스크린샷과 비교
|
|
575
|
+
- 이미지 빠진 거 없나?
|
|
576
|
+
- 텍스트 빠진 거 없나?
|
|
577
|
+
- 레이아웃 구조가 스크린샷과 맞나?
|
|
578
|
+
- Phase 1의 기능 요소가 보존되었나?
|
|
224
579
|
```
|
|
225
580
|
|
|
226
|
-
###
|
|
581
|
+
### 3-2. 코드 작성 규칙
|
|
582
|
+
|
|
583
|
+
```
|
|
584
|
+
컴포넌트 (Vue 예시):
|
|
585
|
+
<template>
|
|
586
|
+
스크린샷에서 보이는 시각 구조대로 작성.
|
|
587
|
+
Figma 레이어 구조 무시.
|
|
588
|
+
Phase 1의 기능 요소(v-for, @click, v-if) 보존.
|
|
589
|
+
시맨틱 HTML 사용 (<section>, <h2>, <ul>, <button>).
|
|
590
|
+
이미지 경로: /images/{feature}/파일명.png
|
|
591
|
+
텍스트: tree.json의 TEXT 노드 characters 값 그대로.
|
|
592
|
+
|
|
593
|
+
<script setup>
|
|
594
|
+
Phase 1의 JSDoc + 인터페이스 + 핸들러 보존.
|
|
595
|
+
새로운 데이터/상태 추가 시 기존과 병합.
|
|
596
|
+
|
|
597
|
+
<style> 블록 없음 — 외부 SCSS만.
|
|
598
|
+
|
|
599
|
+
SCSS (vibe.figma.convert 참조):
|
|
600
|
+
layout/ → position, display, flex, width, height, padding, gap
|
|
601
|
+
components/ → font, color, border, shadow, opacity
|
|
602
|
+
모든 수치는 재료함의 정확한 값 × scaleFactor.
|
|
603
|
+
추정 금지 — 값이 없으면 tree.json에서 다시 찾는다.
|
|
604
|
+
```
|
|
227
605
|
|
|
228
|
-
|
|
229
|
-
**단, 첫 번째 섹션(Hero)은 단독 완료 후 나머지를 병렬로.**
|
|
606
|
+
### 반응형 (추가 URL)
|
|
230
607
|
|
|
231
|
-
|
|
608
|
+
```
|
|
609
|
+
완료 후 질문: "다음 브레이크포인트 디자인 URL을 입력해주세요. (없으면 '없음')"
|
|
232
610
|
|
|
611
|
+
URL 있으면:
|
|
612
|
+
1. 새 URL에서 재료 확보 (Phase 1 반복)
|
|
613
|
+
2. 새 스크린샷과 기존 코드 비교
|
|
614
|
+
3. @media (min-width: $bp-desktop) 오버라이드만 추가
|
|
615
|
+
4. 기존 모바일 코드 삭제 금지
|
|
233
616
|
```
|
|
234
|
-
# [FIGMA_SCRIPT] = ~/.vibe/hooks/scripts/figma-extract.js
|
|
235
|
-
node "[FIGMA_SCRIPT]" render {fileKey} {섹션.nodeId} --out=/tmp/{feature}-{section}/ --depth=10
|
|
236
617
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
├── {section}.scss ← 전체 SCSS (모든 CSS 속성)
|
|
241
|
-
├── {section}.json ← 원본 트리 JSON
|
|
242
|
-
├── {section}-screenshot.png ← 섹션 스크린샷 (시각 기준점)
|
|
243
|
-
└── images/
|
|
244
|
-
├── {section}-bg-composite.png ← 복합 BG 합성 이미지
|
|
245
|
-
├── {section}-title.png ← 이름 있는 이미지
|
|
246
|
-
└── ...
|
|
618
|
+
---
|
|
619
|
+
|
|
620
|
+
## Phase 3.5: 컴파일 게이트
|
|
247
621
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
622
|
+
**Phase 3 퍼즐 조립 완료 후, 브라우저 검증 전에 컴파일 성공을 보장한다.**
|
|
623
|
+
**컴파일 에러는 스킵 불가 — 반드시 수정 또는 사용자 보고.**
|
|
624
|
+
**Phase 3.5 실패 시 Phase 4 진행 불가 (hard gate).**
|
|
625
|
+
|
|
626
|
+
```
|
|
627
|
+
자동 반복: 컴파일 성공까지. 최대 3라운드.
|
|
251
628
|
```
|
|
252
629
|
|
|
253
|
-
|
|
630
|
+
### 3.5-0. 베이스라인 캡처 (Phase 3 변경 전)
|
|
254
631
|
|
|
255
632
|
```
|
|
256
|
-
|
|
633
|
+
Phase 3 시작 전에 기존 프로젝트의 에러를 캡처:
|
|
634
|
+
1. 타입 체크 베이스라인: (3.5-1에서 선택한 동일 명령 사용) > /tmp/{feature}/baseline-typecheck.txt 2>&1
|
|
635
|
+
2. 빌드 베이스라인: npm run build > /tmp/{feature}/baseline-build.txt 2>&1
|
|
257
636
|
|
|
258
|
-
|
|
259
|
-
|
|
637
|
+
Phase 3.5에서는 baseline에 없는 **새로 발생한 에러만** 수정 대상.
|
|
638
|
+
baseline에 존재하던 에러는 무시하고 별도 보고 ("기존 에러 {N}개 유지").
|
|
639
|
+
vibe.figma가 생성/수정한 파일 외의 에러는 자동 수정 금지.
|
|
640
|
+
```
|
|
260
641
|
|
|
261
|
-
|
|
262
|
-
{section}.scss를 읽고 scaleFactor 적용하여 프로젝트 SCSS에 Write:
|
|
263
|
-
styles/{feature}/layout/_{section}.scss ← position, display, flex, width, height
|
|
264
|
-
styles/{feature}/components/_{section}.scss ← font, color, border, shadow
|
|
265
|
-
styles/{feature}/_tokens.scss ← 새 값 추가 (primitive/semantic, vibe.figma.convert 참조)
|
|
266
|
-
index.scss에 새 섹션 @import 추가.
|
|
642
|
+
### 3.5-1. TypeScript 컴파일 체크
|
|
267
643
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
644
|
+
```
|
|
645
|
+
1. 프로젝트 타입 체커 감지 → 실행:
|
|
646
|
+
- package.json scripts에 type-check 또는 typecheck 존재 → npm run type-check 사용
|
|
647
|
+
- vue-tsc 설치 확인 (Vue 프로젝트) → npx vue-tsc --noEmit 2>&1
|
|
648
|
+
- svelte-check 설치 확인 (Svelte 프로젝트) → npx svelte-check 2>&1
|
|
649
|
+
- 위 해당 없음 → fallback: npx tsc --noEmit 2>&1
|
|
650
|
+
→ 에러 0개: PASS → 다음 단계
|
|
651
|
+
→ 에러 있음: 에러 메시지 파싱 → 자동 수정
|
|
652
|
+
|
|
653
|
+
2. 에러 파싱:
|
|
654
|
+
각 에러에서 추출: 파일 경로, 줄 번호, 에러 코드, 메시지
|
|
655
|
+
예: "src/components/Hero.tsx(15,3): error TS2322: Type 'string' is not assignable to type 'number'"
|
|
656
|
+
|
|
657
|
+
3. 자동 수정 (에러 유형별):
|
|
658
|
+
- TS2322 (타입 불일치): prop 타입을 올바르게 수정
|
|
659
|
+
- TS2304 (이름 없음): import 추가
|
|
660
|
+
- TS2339 (프로퍼티 없음): interface에 프로퍼티 추가
|
|
661
|
+
- TS7006 (암시적 any): 타입 어노테이션 추가
|
|
662
|
+
- 기타: Read로 해당 파일+줄 확인 → 컨텍스트 기반 수정
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### 3.5-2. 빌드 체크
|
|
274
666
|
|
|
275
|
-
|
|
276
|
-
|
|
667
|
+
```
|
|
668
|
+
1. npm run build 실행:
|
|
669
|
+
Bash: npm run build 2>&1
|
|
670
|
+
→ 성공: PASS → 다음 단계
|
|
671
|
+
→ 실패: 에러 메시지 파싱 → 자동 수정
|
|
672
|
+
→ 타임아웃: 최대 120초 (초과 시 해당 라운드 실패 처리)
|
|
673
|
+
|
|
674
|
+
2. 일반적 빌드 에러 처리:
|
|
675
|
+
- SCSS 컴파일 에러: 변수명/import 오류 수정
|
|
676
|
+
- Module not found: import 경로 수정 (.js 확장자 등)
|
|
677
|
+
- ESLint 에러 (--max-warnings 초과): 자동 수정 가능한 것 처리
|
|
277
678
|
```
|
|
278
679
|
|
|
279
|
-
|
|
680
|
+
### 3.5-3. dev 서버 시작 확인
|
|
280
681
|
|
|
281
682
|
```
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
683
|
+
1. dev 서버 시작 + PID 캡처:
|
|
684
|
+
Bash: npm run dev & echo $! → DEV_PID 저장
|
|
685
|
+
→ localhost 포트 자동 감지: npm run dev stdout에서 localhost:\d+ 또는 port \d+ 파싱
|
|
686
|
+
감지 실패 시 기본값 3000, 5173, 4173 순서 시도
|
|
687
|
+
→ 포트 폴링 (3초 간격, 최대 30초 대기)
|
|
688
|
+
→ 성공: Phase 4 진행 (Phase 4 완료 후 정리)
|
|
689
|
+
→ 실패: kill $DEV_PID → 에러 로그 확인 → 수정 → 재시도
|
|
690
|
+
|
|
691
|
+
2. 프로세스 정리 규칙:
|
|
692
|
+
- Phase 4 완료 또는 3라운드 실패 시 반드시 정리
|
|
693
|
+
- 정리 순서: kill $DEV_PID → 3초 대기 → kill -9 $DEV_PID (응답 없으면)
|
|
694
|
+
- lsof -i :{port} -t 로 포트 점유 프로세스 확인 후 추가 정리
|
|
695
|
+
※ spawned child process만 대상 — 관련 없는 프로세스 kill 금지
|
|
696
|
+
- interrupt 시에도 cleanup 보장
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### 3.5-4. 수정 루프
|
|
700
|
+
|
|
701
|
+
```
|
|
702
|
+
라운드 1~3:
|
|
703
|
+
1. tsc → build → dev 순서로 체크
|
|
704
|
+
2. 첫 번째 실패 단계의 에러 수정
|
|
705
|
+
3. 수정 후 해당 단계부터 재체크
|
|
706
|
+
4. 모든 단계 통과: Phase 4 진행
|
|
707
|
+
|
|
708
|
+
라운드 종료 조건:
|
|
709
|
+
- 3라운드 후 실패: 에러 목록 + 시도한 수정을 사용자에게 보고
|
|
710
|
+
- 같은 에러 반복: 해당 에러 스킵 불가 → 사용자 보고 (컴파일 에러는 스킵 불가)
|
|
711
|
+
|
|
712
|
+
컴파일 게이트 결과 보고:
|
|
713
|
+
|
|
714
|
+
✅ 통과:
|
|
715
|
+
"Phase 3.5: 컴파일 게이트 PASS (라운드 {N})"
|
|
716
|
+
- tsc: 0 errors
|
|
717
|
+
- build: success
|
|
718
|
+
- dev server: running on localhost:{port}
|
|
719
|
+
|
|
720
|
+
❌ 실패 (3라운드 후):
|
|
721
|
+
"Phase 3.5: 컴파일 게이트 FAIL"
|
|
722
|
+
- 남은 에러 목록 (파일, 줄, 메시지)
|
|
723
|
+
- 시도한 수정 내역
|
|
724
|
+
- 사용자 수동 수정 필요
|
|
725
|
+
→ Phase 4 진행하지 않음
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
---
|
|
285
729
|
|
|
286
|
-
|
|
287
|
-
□ 외부 SCSS 파일에 font-size, color 존재
|
|
288
|
-
□ 이미지 파일 존재 + 0byte 없음
|
|
730
|
+
## Phase 4: 검증 루프
|
|
289
731
|
|
|
290
|
-
|
|
291
|
-
|
|
732
|
+
**Puppeteer + CDP로 실제 렌더링 결과를 확인하며 자동 수정한다.**
|
|
733
|
+
**사람이 브라우저 보면서 고치는 것과 동일한 루프.**
|
|
734
|
+
**인프라: `src/infra/lib/browser/` (범용 UI 검증 도구)**
|
|
292
735
|
|
|
293
|
-
|
|
736
|
+
```
|
|
737
|
+
자동 반복: P1=0 될 때까지. 최대 3라운드.
|
|
294
738
|
```
|
|
295
739
|
|
|
296
|
-
###
|
|
740
|
+
### 4-0. 환경 준비
|
|
297
741
|
|
|
298
742
|
```
|
|
299
|
-
|
|
743
|
+
1. dev 서버 시작:
|
|
744
|
+
Phase 3.5에서 이미 시작된 dev 서버 사용 (재시작 불필요)
|
|
745
|
+
→ localhost:{port} 확인
|
|
746
|
+
|
|
747
|
+
2. Puppeteer 브라우저 시작:
|
|
748
|
+
import { launchBrowser, openPage } from 'src/infra/lib/browser'
|
|
749
|
+
const browser = await launchBrowser({ headless: true })
|
|
750
|
+
const page = await openPage(browser, 'http://localhost:3000/{feature}', {
|
|
751
|
+
width: 480, // 모바일 퍼스트 (또는 타겟 뷰포트)
|
|
752
|
+
height: 960,
|
|
753
|
+
})
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
### 4-1. 렌더링 스크린샷 vs Figma 스크린샷
|
|
300
757
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
758
|
+
```
|
|
759
|
+
import { captureScreenshot, compareScreenshots } from 'src/infra/lib/browser'
|
|
760
|
+
|
|
761
|
+
각 섹션에 대해:
|
|
762
|
+
1. 렌더링 결과 스크린샷 캡처:
|
|
763
|
+
await captureScreenshot(page, {
|
|
764
|
+
outPath: '/tmp/{feature}/rendered-{section}.png',
|
|
765
|
+
selector: '.{section}Section', // Phase 1에서 만든 클래스
|
|
766
|
+
})
|
|
767
|
+
|
|
768
|
+
2. Figma 원본과 픽셀 비교:
|
|
769
|
+
const diff = await compareScreenshots(
|
|
770
|
+
'/tmp/{feature}/sections/{section}.png', // Figma 원본
|
|
771
|
+
'/tmp/{feature}/rendered-{section}.png', // 렌더링 결과
|
|
772
|
+
'/tmp/{feature}/diff-{section}.png', // 차이 시각화
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
3. diff 이미지를 Read로 확인:
|
|
776
|
+
→ 빨간색 영역 = 차이 나는 부분
|
|
777
|
+
→ diffRatio > 0.1 이면 P1 이슈
|
|
306
778
|
```
|
|
307
779
|
|
|
308
|
-
|
|
780
|
+
### 4-2. CSS 수치 정밀 비교
|
|
781
|
+
|
|
782
|
+
```
|
|
783
|
+
import { getComputedStyles, compareStyles, diffsToIssues } from 'src/infra/lib/browser'
|
|
784
|
+
|
|
785
|
+
각 섹션의 주요 요소에 대해:
|
|
786
|
+
1. 렌더링된 computed CSS 추출:
|
|
787
|
+
const actual = await getComputedStyles(page, '.heroTitle', [
|
|
788
|
+
'font-size', 'color', 'width', 'height', 'padding', 'margin',
|
|
789
|
+
'background-color', 'border-radius', 'gap',
|
|
790
|
+
])
|
|
791
|
+
|
|
792
|
+
2. Figma 재료함의 기대값과 비교:
|
|
793
|
+
// tree.json에서 해당 노드의 CSS 수치 (scaleFactor 적용 후)
|
|
794
|
+
const expected = { 'font-size': '16px', 'color': '#ffffff', 'width': '465px' }
|
|
795
|
+
const diffs = compareStyles(expected, actual)
|
|
796
|
+
|
|
797
|
+
3. 차이 → 이슈 변환:
|
|
798
|
+
const issues = diffsToIssues(diffs)
|
|
799
|
+
→ delta > 4px: P1 (레이아웃 영향)
|
|
800
|
+
→ delta ≤ 4px: P2 (미세 차이)
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
### 4-3. 이미지·텍스트 누락 체크
|
|
804
|
+
|
|
805
|
+
```
|
|
806
|
+
import { extractImages, extractTextContent } from 'src/infra/lib/browser'
|
|
807
|
+
|
|
808
|
+
1. 이미지 로드 상태 확인:
|
|
809
|
+
const images = await extractImages(page)
|
|
810
|
+
images.filter(img => !img.loaded)
|
|
811
|
+
→ 로드 실패 이미지 = P1 (이미지 누락)
|
|
812
|
+
→ src="" 이미지 = P1 (빈 경로)
|
|
813
|
+
|
|
814
|
+
2. 텍스트 콘텐츠 확인:
|
|
815
|
+
const texts = await extractTextContent(page)
|
|
816
|
+
→ 재료함의 TEXT 노드 characters와 대조
|
|
817
|
+
→ 누락된 텍스트 = P1
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
### 4-4. 자동 수정 루프
|
|
821
|
+
|
|
822
|
+
```
|
|
823
|
+
라운드 1~3:
|
|
824
|
+
1. 4-1 ~ 4-3 실행 → 이슈 목록 수집
|
|
825
|
+
2. P1 이슈 우선 수정:
|
|
826
|
+
- 이미지 누락 → 이미지 경로 확인, static/ 에 파일 존재 확인
|
|
827
|
+
- 레이아웃 다름 → 스크린샷 diff 이미지 + computed CSS로 원인 파악
|
|
828
|
+
- 텍스트 누락 → 재료함의 정확한 텍스트 삽입
|
|
829
|
+
- CSS 수치 틀림 → 재료함(tree.json)의 정확한 값으로 교체
|
|
830
|
+
⚠️ 추정으로 수정하지 않는다. 반드시 재료함 참조.
|
|
831
|
+
3. 수정 후 컴파일 재검증:
|
|
832
|
+
Bash: npx tsc --noEmit 2>&1 (또는 3.5-1에서 선택한 타입 체커)
|
|
833
|
+
→ 시각 수정이 타입 에러를 유발하면 즉시 타입 에러 수정 후 진행
|
|
834
|
+
4. 페이지 리로드 → 다시 캡처 → 비교
|
|
835
|
+
5. P1=0 이면 종료
|
|
836
|
+
|
|
837
|
+
라운드 종료 조건:
|
|
838
|
+
- P1=0: 성공 → 브라우저 종료, 결과 보고
|
|
839
|
+
- 3라운드 후 P1 남음: TODO 목록으로 사용자에게 보고
|
|
840
|
+
- 같은 이슈가 반복: 해당 이슈 스킵, 다음 이슈로
|
|
841
|
+
|
|
842
|
+
결과 보고:
|
|
843
|
+
- 수정한 파일 목록
|
|
844
|
+
- 남은 P2 이슈 목록 (선택적 수정)
|
|
845
|
+
- 최종 diff 스크린샷 경로
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
### 4-5. 반응형 검증 (추가 뷰포트)
|
|
849
|
+
|
|
850
|
+
```
|
|
851
|
+
모바일 검증 완료 후, 추가 브레이크포인트가 있으면:
|
|
852
|
+
|
|
853
|
+
await page.setViewport({ width: 1920, height: 1080 })
|
|
854
|
+
await page.reload({ waitUntil: 'networkidle0' })
|
|
855
|
+
|
|
856
|
+
→ 데스크탑 Figma 스크린샷과 동일한 4-1 ~ 4-4 루프 반복
|
|
857
|
+
```
|
|
309
858
|
|
|
310
|
-
|
|
859
|
+
### 4-6. 브라우저 정리
|
|
311
860
|
|
|
312
861
|
```
|
|
313
|
-
|
|
314
|
-
□ "<style" in components/{feature}/ → 0건
|
|
315
|
-
□ 'src=""' in components/{feature}/ → 0건
|
|
316
|
-
□ Glob: images/{feature}/ → 이미지 파일 존재
|
|
862
|
+
import { closeBrowser } from 'src/infra/lib/browser'
|
|
317
863
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
→ dev 서버/preview와 비교
|
|
322
|
-
P1 (필수): 이미지 누락, 레이아웃 구조 다름, 텍스트 스타일 미적용
|
|
323
|
-
P2 (권장): 미세 간격, 미세 색상 차이
|
|
324
|
-
→ P1 수정 → 재검증 (P1=0 될 때까지)
|
|
864
|
+
검증 완료 후:
|
|
865
|
+
await closeBrowser()
|
|
866
|
+
dev 서버 종료 (필요 시)
|
|
325
867
|
```
|