@su-record/vibe 2.8.46 → 2.8.48
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/hooks/scripts/figma-extract.js +60 -20
- package/package.json +1 -1
- package/skills/vibe.figma/SKILL.md +116 -28
- package/skills/vibe.figma/templates/figma-handoff.md +1 -1
- package/skills/vibe.figma/templates/remapped-tree.md +8 -8
- package/skills/vibe.figma.convert/SKILL.md +62 -20
- package/skills/vibe.figma.convert/rubrics/conversion-rules.md +49 -3
- package/skills/vibe.figma.convert/templates/component.md +2 -2
- package/skills/vibe.figma.extract/SKILL.md +67 -20
- package/skills/vibe.figma.extract/rubrics/image-rules.md +65 -9
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import fs from 'fs';
|
|
16
16
|
import path from 'path';
|
|
17
17
|
import os from 'os';
|
|
18
|
+
import { execFileSync } from 'child_process';
|
|
18
19
|
|
|
19
20
|
// ─── Config ─────────────────────────────────────────────────────────
|
|
20
21
|
|
|
@@ -22,6 +23,39 @@ const FIGMA_API = 'https://api.figma.com/v1';
|
|
|
22
23
|
const MAX_RETRIES = 3;
|
|
23
24
|
const INITIAL_DELAY_MS = 2000;
|
|
24
25
|
|
|
26
|
+
// ─── WebP 변환 ──────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
let _cwebpAvailable;
|
|
29
|
+
function hasCwebp() {
|
|
30
|
+
if (_cwebpAvailable === undefined) {
|
|
31
|
+
try { execFileSync('cwebp', ['-version'], { stdio: 'ignore' }); _cwebpAvailable = true; }
|
|
32
|
+
catch { _cwebpAvailable = false; }
|
|
33
|
+
}
|
|
34
|
+
return _cwebpAvailable;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** PNG 버퍼를 webp 파일로 저장. cwebp 없으면 png 폴백. */
|
|
38
|
+
function writeAsWebp(pngBuf, outPath) {
|
|
39
|
+
if (!hasCwebp()) {
|
|
40
|
+
// cwebp 없으면 png 폴백
|
|
41
|
+
const fallback = outPath.replace(/\.webp$/, '.png');
|
|
42
|
+
fs.writeFileSync(fallback, pngBuf);
|
|
43
|
+
return fallback;
|
|
44
|
+
}
|
|
45
|
+
const tmpPng = outPath + '.tmp.png';
|
|
46
|
+
fs.writeFileSync(tmpPng, pngBuf);
|
|
47
|
+
try {
|
|
48
|
+
execFileSync('cwebp', ['-q', '85', tmpPng, '-o', outPath], { stdio: 'ignore' });
|
|
49
|
+
fs.unlinkSync(tmpPng);
|
|
50
|
+
return outPath;
|
|
51
|
+
} catch {
|
|
52
|
+
// 변환 실패 시 png 폴백
|
|
53
|
+
const fallback = outPath.replace(/\.webp$/, '.png');
|
|
54
|
+
fs.renameSync(tmpPng, fallback);
|
|
55
|
+
return fallback;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
25
59
|
function loadToken() {
|
|
26
60
|
if (process.env.FIGMA_ACCESS_TOKEN) return process.env.FIGMA_ACCESS_TOKEN;
|
|
27
61
|
const configPath = path.join(os.homedir(), '.vibe', 'config.json');
|
|
@@ -178,11 +212,10 @@ async function cmdImages(token, fk, nid, outDir, depth) {
|
|
|
178
212
|
for (const ref of refs) {
|
|
179
213
|
const url = urls[ref];
|
|
180
214
|
if (!url) continue;
|
|
181
|
-
const
|
|
215
|
+
const outWebp = path.join(outDir, ref.slice(0,16) + '.webp');
|
|
182
216
|
dl.push(fetch(url).then(r=>r.arrayBuffer()).then(b=>{
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (sz > 0) imageMap[ref] = out;
|
|
217
|
+
const actual = writeAsWebp(Buffer.from(b), outWebp);
|
|
218
|
+
if (fs.statSync(actual).size > 0) imageMap[ref] = actual;
|
|
186
219
|
}).catch(()=>{}));
|
|
187
220
|
}
|
|
188
221
|
await Promise.all(dl);
|
|
@@ -200,8 +233,14 @@ async function cmdScreenshot(token, fk, nid, outPath) {
|
|
|
200
233
|
const url = data.images?.[nid];
|
|
201
234
|
if (!url) continue;
|
|
202
235
|
const buf = Buffer.from(await (await fetch(url)).arrayBuffer());
|
|
203
|
-
|
|
204
|
-
|
|
236
|
+
let finalPath = outPath;
|
|
237
|
+
if (outPath.endsWith('.webp')) {
|
|
238
|
+
finalPath = writeAsWebp(buf, outPath);
|
|
239
|
+
} else {
|
|
240
|
+
fs.writeFileSync(outPath, buf);
|
|
241
|
+
}
|
|
242
|
+
const sz = fs.statSync(finalPath).size;
|
|
243
|
+
console.log(JSON.stringify({ path: finalPath, size: sz, scale }));
|
|
205
244
|
return;
|
|
206
245
|
} catch (e) {
|
|
207
246
|
if (scale === 1) fail(`Screenshot failed: ${e.message}`);
|
|
@@ -301,7 +340,7 @@ function toSCSS(node, prefix, indent = 0) {
|
|
|
301
340
|
function buildImageNames(node, prefix, result = {}) {
|
|
302
341
|
if (node.imageRef) {
|
|
303
342
|
const name = nodeName(node, prefix);
|
|
304
|
-
result[node.imageRef] = name + '.
|
|
343
|
+
result[node.imageRef] = name + '.webp';
|
|
305
344
|
}
|
|
306
345
|
if (node.children) {
|
|
307
346
|
for (const child of node.children) {
|
|
@@ -339,12 +378,12 @@ async function cmdRender(token, fk, nid, outDir, depth, scale) {
|
|
|
339
378
|
for (const ref of refs) {
|
|
340
379
|
const url = urls[ref];
|
|
341
380
|
if (!url) continue;
|
|
342
|
-
const fileName = imageNames[ref] || ref.slice(0, 16) + '.
|
|
381
|
+
const fileName = imageNames[ref] || ref.slice(0, 16) + '.webp';
|
|
343
382
|
const filePath = path.join(imgDir, fileName);
|
|
344
|
-
const publicPath = `images/${fileName}`;
|
|
345
383
|
dl.push(fetch(url).then(r => r.arrayBuffer()).then(b => {
|
|
346
|
-
|
|
347
|
-
|
|
384
|
+
const actual = writeAsWebp(Buffer.from(b), filePath);
|
|
385
|
+
const actualName = path.basename(actual);
|
|
386
|
+
if (fs.statSync(actual).size > 0) imageMap[ref] = `images/${actualName}`;
|
|
348
387
|
}).catch(() => {}));
|
|
349
388
|
}
|
|
350
389
|
await Promise.all(dl);
|
|
@@ -353,7 +392,7 @@ async function cmdRender(token, fk, nid, outDir, depth, scale) {
|
|
|
353
392
|
// 4. 복합 BG 노드 → 스크린샷으로 렌더링
|
|
354
393
|
for (const child of tree.children || []) {
|
|
355
394
|
if (/^(BG|bg|배경)$/i.test(child.name) && child.children?.length > 3) {
|
|
356
|
-
const bgName = `${sectionPrefix}-bg-composite.
|
|
395
|
+
const bgName = `${sectionPrefix}-bg-composite.webp`;
|
|
357
396
|
const bgPath = path.join(imgDir, bgName);
|
|
358
397
|
try {
|
|
359
398
|
for (const s of [2, 1]) {
|
|
@@ -361,12 +400,12 @@ async function cmdRender(token, fk, nid, outDir, depth, scale) {
|
|
|
361
400
|
const sUrl = sData.images?.[child.nodeId];
|
|
362
401
|
if (!sUrl) continue;
|
|
363
402
|
const buf = Buffer.from(await (await fetch(sUrl)).arrayBuffer());
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
403
|
+
const actual = writeAsWebp(buf, bgPath);
|
|
404
|
+
const actualName = path.basename(actual);
|
|
405
|
+
if (fs.statSync(actual).size > 0) {
|
|
406
|
+
imageMap[`__bg_${child.nodeId}`] = `images/${actualName}`;
|
|
368
407
|
child.imageRef = `__bg_${child.nodeId}`;
|
|
369
|
-
child.children = [];
|
|
408
|
+
child.children = [];
|
|
370
409
|
break;
|
|
371
410
|
}
|
|
372
411
|
}
|
|
@@ -375,14 +414,15 @@ async function cmdRender(token, fk, nid, outDir, depth, scale) {
|
|
|
375
414
|
}
|
|
376
415
|
|
|
377
416
|
// 5. 스크린샷
|
|
378
|
-
const screenshotPath = path.join(outDir, `${sectionPrefix}-screenshot.
|
|
417
|
+
const screenshotPath = path.join(outDir, `${sectionPrefix}-screenshot.webp`);
|
|
418
|
+
let actualScreenshot = screenshotPath;
|
|
379
419
|
try {
|
|
380
420
|
for (const s of [2, 1]) {
|
|
381
421
|
const sData = await apiFetch(`/images/${fk}?ids=${nid}&format=png&scale=${s}`, token);
|
|
382
422
|
const sUrl = sData.images?.[nid];
|
|
383
423
|
if (!sUrl) continue;
|
|
384
424
|
const buf = Buffer.from(await (await fetch(sUrl)).arrayBuffer());
|
|
385
|
-
|
|
425
|
+
actualScreenshot = writeAsWebp(buf, screenshotPath);
|
|
386
426
|
break;
|
|
387
427
|
}
|
|
388
428
|
} catch { /* screenshot failed */ }
|
|
@@ -405,7 +445,7 @@ async function cmdRender(token, fk, nid, outDir, depth, scale) {
|
|
|
405
445
|
html: `${sectionPrefix}.html`,
|
|
406
446
|
scss: `${sectionPrefix}.scss`,
|
|
407
447
|
json: `${sectionPrefix}.json`,
|
|
408
|
-
screenshot:
|
|
448
|
+
screenshot: path.basename(actualScreenshot),
|
|
409
449
|
},
|
|
410
450
|
images: imageMap,
|
|
411
451
|
imageCount: Object.keys(imageMap).length,
|
package/package.json
CHANGED
|
@@ -18,6 +18,37 @@ Figma 트리가 코드의 원천이다. 스크린샷은 검증용이다.
|
|
|
18
18
|
✅ 스크린샷은 생성이 아닌 검증에만 사용
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## ⛔ 불변 규칙 (이 규칙을 위반하면 전체 결과물이 무효)
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
이 규칙들은 어떤 상황에서도 예외 없이 적용된다.
|
|
25
|
+
"복잡해서", "시간이 없어서", "렌더링이 안 돼서" 등의 이유로 우회할 수 없다.
|
|
26
|
+
|
|
27
|
+
1. screenshot 명령으로 콘텐츠를 이미지화하는 것은 금지한다.
|
|
28
|
+
screenshot 명령의 허용 범위:
|
|
29
|
+
✅ BG 프레임 렌더링 (TEXT 자식이 없는 순수 배경)
|
|
30
|
+
✅ 벡터 글자 GROUP 렌더링 (웹폰트 없는 장식 타이틀)
|
|
31
|
+
✅ 섹션별 스크린샷 → sections/ 폴더 (Phase 6 검증용)
|
|
32
|
+
screenshot 명령의 금지 범위:
|
|
33
|
+
❌ TEXT 자식이 있는 프레임 → HTML로 구현
|
|
34
|
+
❌ INSTANCE 반복 패턴이 있는 프레임 → HTML v-for로 구현
|
|
35
|
+
❌ 버튼/가격/수량이 있는 프레임 → HTML로 구현
|
|
36
|
+
❌ "섹션 콘텐츠"를 통째 렌더링 → HTML로 구현
|
|
37
|
+
|
|
38
|
+
위반 시 발생하는 문제:
|
|
39
|
+
exchange-section1.webp (카드 4개를 이미지 1장으로) → 가격 수정 불가, 버튼 클릭 불가
|
|
40
|
+
daily-step2-list.webp (보상 목록을 이미지로) → 수량 변경 불가, 접근성 불가
|
|
41
|
+
|
|
42
|
+
2. BG를 <img> 태그로 넣는 것은 금지한다.
|
|
43
|
+
❌ <img src="bg.webp"> 또는 <div class="bg"><img src="bg.webp"></div>
|
|
44
|
+
✅ .section { background-image: url('bg.webp'); background-size: cover; }
|
|
45
|
+
|
|
46
|
+
3. Phase 4 코드 생성 중 새로운 screenshot 호출은 금지한다.
|
|
47
|
+
Phase 2에서 확보한 재료만 사용한다.
|
|
48
|
+
"이 부분이 복잡하니 screenshot으로 이미지화하자"는 금지된 사고방식이다.
|
|
49
|
+
복잡한 UI도 tree.json의 구조를 따라 HTML+CSS로 구현한다.
|
|
50
|
+
```
|
|
51
|
+
|
|
21
52
|
## 금지 사항
|
|
22
53
|
|
|
23
54
|
```
|
|
@@ -30,6 +61,22 @@ Figma 트리가 코드의 원천이다. 스크린샷은 검증용이다.
|
|
|
30
61
|
❌ 컴포넌트 파일 안에 <style> 블록 / 인라인 style=""
|
|
31
62
|
✅ tree.json의 CSS 속성을 SCSS에 직접 매핑
|
|
32
63
|
✅ 외부 SCSS 파일에만 스타일 작성
|
|
64
|
+
|
|
65
|
+
━━━ 이미지 vs HTML 판별 (가장 흔한 실수) ━━━
|
|
66
|
+
|
|
67
|
+
❌ BG를 <img> 태그로 처리 → 반드시 CSS background-image
|
|
68
|
+
잘못된 예: <img src="hero-bg.webp" class="bg-img" />
|
|
69
|
+
올바른 예: .section { background-image: url('hero-bg.webp'); background-size: cover; }
|
|
70
|
+
|
|
71
|
+
❌ 카드/아이템 그리드를 통째 이미지 1장으로 렌더링
|
|
72
|
+
잘못된 예: <img src="exchange-section1.webp" /> (카드 4개가 한 이미지)
|
|
73
|
+
올바른 예: v-for로 개별 카드 HTML 생성 + 내부 아이콘만 <img>
|
|
74
|
+
|
|
75
|
+
❌ TEXT 자식이 있는 프레임을 이미지로 렌더링
|
|
76
|
+
잘못된 예: <img src="daily-step2-list.webp" /> (텍스트+카드 포함)
|
|
77
|
+
올바른 예: HTML로 텍스트/레이아웃 구현 + 순수 이미지 에셋만 <img>
|
|
78
|
+
|
|
79
|
+
✅ 이미지로 렌더링 가능한 것: 벡터 글자, 합성 BG(텍스트 미포함), 래스터 에셋, 복잡 벡터
|
|
33
80
|
```
|
|
34
81
|
|
|
35
82
|
## 전체 플로우
|
|
@@ -355,7 +402,7 @@ URL 개수에 따른 처리:
|
|
|
355
402
|
URL에서 fileKey, nodeId 추출
|
|
356
403
|
|
|
357
404
|
1단계 — 전체 스크린샷 (정답 사진):
|
|
358
|
-
node "[FIGMA_SCRIPT]" screenshot {fileKey} {nodeId} --out=/tmp/{feature}/full-screenshot.
|
|
405
|
+
node "[FIGMA_SCRIPT]" screenshot {fileKey} {nodeId} --out=/tmp/{feature}/full-screenshot.webp
|
|
359
406
|
|
|
360
407
|
2단계 — 전체 트리 + CSS (수치 재료):
|
|
361
408
|
node "[FIGMA_SCRIPT]" tree {fileKey} {nodeId} --depth=10
|
|
@@ -365,19 +412,26 @@ URL에서 fileKey, nodeId 추출
|
|
|
365
412
|
node "[FIGMA_SCRIPT]" images {fileKey} {nodeId} --out=/tmp/{feature}/images/ --depth=10
|
|
366
413
|
→ 모든 이미지 에셋 확보. 누락 0건, 0byte 0건.
|
|
367
414
|
|
|
368
|
-
3.5단계 —
|
|
369
|
-
tree.json에서
|
|
370
|
-
-
|
|
415
|
+
3.5단계 — 개별 에셋 노드 렌더링 (추가 시각 재료):
|
|
416
|
+
tree.json에서 이미지 에셋 후보를 식별:
|
|
417
|
+
- 아이콘: VECTOR/GROUP 크기 ≤ 64px
|
|
418
|
+
- 아이템 썸네일: name에 "item", "icon", "reward", "token", "coin", "badge"
|
|
371
419
|
- 크기 50~300px 범위의 독립 요소
|
|
372
420
|
- fill 이미지가 없지만 시각적으로 의미 있는 노드
|
|
421
|
+
|
|
422
|
+
⛔ 렌더링 전 TEXT 자식 검증 (BLOCKING):
|
|
423
|
+
후보 노드의 자식 트리에 TEXT 노드가 있으면 → 렌더링 대상에서 제외
|
|
424
|
+
후보 노드가 INSTANCE 반복 패턴의 부모이면 → 렌더링 대상에서 제외
|
|
425
|
+
"카드", "리스트", "그리드" 등 복합 UI를 통째 렌더링하지 않는다
|
|
426
|
+
|
|
373
427
|
해당 노드를 개별 렌더링:
|
|
374
428
|
node "[FIGMA_SCRIPT]" images {fileKey} {nodeId} --render --nodeIds={id1},{id2},... --out=/tmp/{feature}/images/
|
|
375
|
-
→ 이미지 fill이 아닌 벡터/인스턴스 에셋도
|
|
376
|
-
→ Phase
|
|
429
|
+
→ 이미지 fill이 아닌 벡터/인스턴스 에셋도 webp로 확보
|
|
430
|
+
→ Phase 4에서 카드 내부의 아이콘/썸네일로 사용
|
|
377
431
|
|
|
378
432
|
4단계 — 섹션별 스크린샷 (부분 정답):
|
|
379
433
|
트리의 1depth 자식 프레임 각각:
|
|
380
|
-
node "[FIGMA_SCRIPT]" screenshot {fileKey} {child.nodeId} --out=/tmp/{feature}/sections/{child.name}.
|
|
434
|
+
node "[FIGMA_SCRIPT]" screenshot {fileKey} {child.nodeId} --out=/tmp/{feature}/sections/{child.name}.webp
|
|
381
435
|
```
|
|
382
436
|
|
|
383
437
|
### 2-1-multi. 멀티 프레임 재료 확보 (2개 이상 URL)
|
|
@@ -399,7 +453,7 @@ URL에서 fileKey, nodeId 추출
|
|
|
399
453
|
결과:
|
|
400
454
|
/tmp/{feature}/
|
|
401
455
|
├── frame-1/ ← 메인 페이지
|
|
402
|
-
│ ├── full-screenshot.
|
|
456
|
+
│ ├── full-screenshot.webp
|
|
403
457
|
│ ├── tree.json
|
|
404
458
|
│ ├── images/
|
|
405
459
|
│ └── sections/
|
|
@@ -417,17 +471,17 @@ Phase 2 완료 시 /tmp/{feature}/ 에 다음이 준비되어야 함:
|
|
|
417
471
|
|
|
418
472
|
단일 URL:
|
|
419
473
|
/tmp/{feature}/
|
|
420
|
-
├── full-screenshot.
|
|
474
|
+
├── full-screenshot.webp ← 전체 정답 사진
|
|
421
475
|
├── tree.json ← 노드 트리 + CSS 수치
|
|
422
476
|
├── images/ ← 모든 이미지 에셋
|
|
423
|
-
│ ├── hero-bg.
|
|
424
|
-
│ ├── hero-title.
|
|
425
|
-
│ ├── card-item-1.
|
|
477
|
+
│ ├── hero-bg.webp
|
|
478
|
+
│ ├── hero-title.webp
|
|
479
|
+
│ ├── card-item-1.webp
|
|
426
480
|
│ └── ...
|
|
427
481
|
└── sections/ ← 섹션별 정답 사진
|
|
428
|
-
├── hero.
|
|
429
|
-
├── daily-checkin.
|
|
430
|
-
├── playtime-mission.
|
|
482
|
+
├── hero.webp
|
|
483
|
+
├── daily-checkin.webp
|
|
484
|
+
├── playtime-mission.webp
|
|
431
485
|
└── ...
|
|
432
486
|
|
|
433
487
|
멀티 URL: 위 2-1-multi 결과 구조 참조
|
|
@@ -471,7 +525,7 @@ UI 요소 → vw 비례:
|
|
|
471
525
|
|
|
472
526
|
```
|
|
473
527
|
1. 시각 비교 — 각 프레임의 스크린샷을 순차 Read:
|
|
474
|
-
- frame-1/full-screenshot.
|
|
528
|
+
- frame-1/full-screenshot.webp → frame-2/full-screenshot.webp → ...
|
|
475
529
|
- 시각적으로 반복되는 요소 식별:
|
|
476
530
|
상단 영역 (GNB/Header), 하단 영역 (Footer),
|
|
477
531
|
카드 패턴, 버튼 스타일, 섹션 레이아웃
|
|
@@ -645,6 +699,14 @@ Phase 1에서 생성한 빈 SCSS 파일에 기본 내용 Write:
|
|
|
645
699
|
|
|
646
700
|
섹션 3 (Daily) → ... 반복
|
|
647
701
|
|
|
702
|
+
⛔ Phase 4에서 screenshot/render 호출 금지:
|
|
703
|
+
Phase 2에서 확보한 이미지만 사용한다.
|
|
704
|
+
"이 섹션이 복잡하니 screenshot으로 이미지화하자" → 금지된 접근.
|
|
705
|
+
복잡한 섹션도 tree.json 구조를 따라 HTML+CSS로 구현한다.
|
|
706
|
+
섹션 내부에 카드 그리드/보상 목록이 있으면:
|
|
707
|
+
→ 카드 내부의 아이콘/썸네일(Phase 2에서 확보)만 <img>
|
|
708
|
+
→ 가격, 수량, 버튼 텍스트, 제목은 모두 HTML
|
|
709
|
+
|
|
648
710
|
각 섹션 완료 시:
|
|
649
711
|
- 해당 섹션의 컴포넌트 + SCSS가 동작하는 상태
|
|
650
712
|
- pages/{feature}.vue에 해당 섹션이 추가된 상태
|
|
@@ -660,16 +722,42 @@ Phase 1에서 생성한 빈 SCSS 파일에 기본 내용 Write:
|
|
|
660
722
|
→ Auto Layout 속성(flex, gap, padding)이 코드의 기반
|
|
661
723
|
→ 스크린샷은 검증용으로만 참조
|
|
662
724
|
|
|
663
|
-
2.
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
725
|
+
2. ⛔ 이미지 vs HTML 판별 (BLOCKING — 코드 작성 전 반드시 실행):
|
|
726
|
+
섹션의 모든 노드를 순회하여 각 노드가 이미지인지 HTML인지 판별한다.
|
|
727
|
+
판별하지 않고 코드를 작성하면 안 된다.
|
|
728
|
+
|
|
729
|
+
판별 결과 테이블을 먼저 작성한 후 코드 생성:
|
|
730
|
+
| 노드 name | 타입 | 판별 | 근거 |
|
|
731
|
+
|-----------|------|------|------|
|
|
732
|
+
| BG | FRAME | CSS background-image | BG 프레임 |
|
|
733
|
+
| Title | TEXT 포함 FRAME | HTML | TEXT 자식 보유 |
|
|
734
|
+
| CardGrid | INSTANCE ×4 | HTML v-for | 반복 패턴 |
|
|
735
|
+
| CoinIcon | RECTANGLE+imageRef | <img> | 순수 에셋 |
|
|
736
|
+
|
|
737
|
+
판별 규칙 (하나라도 YES → HTML):
|
|
738
|
+
Q1. TEXT 자식 있는가? → YES → HTML (텍스트를 이미지에 넣지 않는다)
|
|
739
|
+
Q2. INSTANCE 반복 패턴인가? → YES → HTML v-for (내부 이미지 에셋만 <img>)
|
|
740
|
+
Q3. 인터랙티브 요소인가? (btn, CTA) → YES → HTML <button>
|
|
741
|
+
Q4. 동적 데이터인가? (가격, 수량, 기간) → YES → HTML 텍스트
|
|
742
|
+
모두 NO → 이미지 렌더링 가능
|
|
743
|
+
|
|
744
|
+
⚠️ 특히 주의: 섹션을 통째로 이미지 렌더링하는 것은 절대 금지.
|
|
745
|
+
섹션 안의 개별 요소를 판별하여 이미지/HTML을 분리해야 한다.
|
|
746
|
+
|
|
747
|
+
3. 기계적 매핑 (추정 없음):
|
|
748
|
+
a. 이미지: 판별에서 "이미지"로 확정된 노드만 static/images/{feature}/에 배치
|
|
749
|
+
b. BG 프레임 처리:
|
|
750
|
+
❌ <img src="bg.webp"> 또는 <div><img src="bg.webp" class="bg"></div> 금지
|
|
751
|
+
✅ 부모에 CSS background-image만 사용:
|
|
752
|
+
.section { background-image: url('/images/{feature}/bg.webp'); background-size: cover; }
|
|
753
|
+
✅ BG 프레임은 HTML에 아무 요소도 렌더링하지 않음
|
|
754
|
+
c. 노드 → HTML 매핑:
|
|
667
755
|
- Auto Layout 있음 → <div> + flex (direction/gap/padding 직접)
|
|
668
756
|
- Auto Layout 없음 → <div> + position:relative (자식 absolute)
|
|
669
757
|
- TEXT 노드 → <span> (Claude가 h2/p/button으로 승격)
|
|
670
|
-
-
|
|
671
|
-
- 반복 패턴 (동일 구조
|
|
672
|
-
|
|
758
|
+
- 순수 이미지 에셋 → <img src="렌더링된 파일">
|
|
759
|
+
- 반복 패턴 (동일 구조 2+) → v-for (카드 내 이미지만 <img>, 나머지 HTML)
|
|
760
|
+
d. CSS 직접 매핑:
|
|
673
761
|
- node.css의 모든 속성을 SCSS에 1:1 매핑
|
|
674
762
|
- vw/clamp 반응형 단위 변환 (vibe.figma.convert 참조)
|
|
675
763
|
- tree.json에 없는 CSS 값은 작성하지 않음
|
|
@@ -696,7 +784,7 @@ Phase 1에서 생성한 빈 SCSS 파일에 기본 내용 Write:
|
|
|
696
784
|
tree.json의 Auto Layout 구조를 HTML flex 레이아웃으로 직접 매핑.
|
|
697
785
|
Claude가 시맨틱 태그로 승격 (div → section/h2/p/button).
|
|
698
786
|
Phase 1의 기능 요소(v-for, @click, v-if) 보존.
|
|
699
|
-
이미지 경로: /images/{feature}/파일명.
|
|
787
|
+
이미지 경로: /images/{feature}/파일명.webp (실제 파일 존재 확인)
|
|
700
788
|
텍스트: tree.json의 TEXT 노드 characters 값 그대로.
|
|
701
789
|
|
|
702
790
|
<script setup>
|
|
@@ -870,15 +958,15 @@ import { captureScreenshot, compareScreenshots } from 'src/infra/lib/browser'
|
|
|
870
958
|
각 섹션에 대해:
|
|
871
959
|
1. 렌더링 결과 스크린샷 캡처:
|
|
872
960
|
await captureScreenshot(page, {
|
|
873
|
-
outPath: '/tmp/{feature}/rendered-{section}.
|
|
961
|
+
outPath: '/tmp/{feature}/rendered-{section}.webp',
|
|
874
962
|
selector: '.{section}Section', // Phase 1에서 만든 클래스
|
|
875
963
|
})
|
|
876
964
|
|
|
877
965
|
2. Figma 원본과 픽셀 비교:
|
|
878
966
|
const diff = await compareScreenshots(
|
|
879
|
-
'/tmp/{feature}/sections/{section}.
|
|
880
|
-
'/tmp/{feature}/rendered-{section}.
|
|
881
|
-
'/tmp/{feature}/diff-{section}.
|
|
967
|
+
'/tmp/{feature}/sections/{section}.webp', // Figma 원본
|
|
968
|
+
'/tmp/{feature}/rendered-{section}.webp', // 렌더링 결과
|
|
969
|
+
'/tmp/{feature}/diff-{section}.webp', // 차이 시각화
|
|
882
970
|
)
|
|
883
971
|
|
|
884
972
|
3. diff 이미지를 Read로 확인:
|
|
@@ -53,7 +53,7 @@ ROOT folder: `{{MO_FOLDER}}/`, `{{PC_FOLDER}}/`
|
|
|
53
53
|
|
|
54
54
|
| Image | Local Path | Type | Render Method |
|
|
55
55
|
|-------|-----------|------|--------------|
|
|
56
|
-
| {{NAME}} | `/images/{{FEATURE_KEY}}/{{FILE_NAME}}.
|
|
56
|
+
| {{NAME}} | `/images/{{FEATURE_KEY}}/{{FILE_NAME}}.webp` | {{bg\|content\|vector-text}} | {{frame-render\|node-render\|group-render}} |
|
|
57
57
|
|
|
58
58
|
Total assets: {{ASSET_COUNT}} (BG 렌더링 {{BG_COUNT}}장 + 콘텐츠 {{CONTENT_COUNT}}장)
|
|
59
59
|
|
|
@@ -43,8 +43,8 @@ tree.json을 코드 생성에 최적화된 구조로 리매핑한다.
|
|
|
43
43
|
"states": ["default"]
|
|
44
44
|
},
|
|
45
45
|
"bg": {
|
|
46
|
-
"mo": { "nodeId": "...", "file": "mo-main/bg/hero-bg.
|
|
47
|
-
"pc": { "nodeId": "...", "file": "pc-main/bg/hero-bg.
|
|
46
|
+
"mo": { "nodeId": "...", "file": "mo-main/bg/hero-bg.webp" },
|
|
47
|
+
"pc": { "nodeId": "...", "file": "pc-main/bg/hero-bg.webp" }
|
|
48
48
|
},
|
|
49
49
|
"size": {
|
|
50
50
|
"mo": { "w": 720, "h": 1280 },
|
|
@@ -63,8 +63,8 @@ tree.json을 코드 생성에 최적화된 구조로 리매핑한다.
|
|
|
63
63
|
"role": "title-image",
|
|
64
64
|
"type": "node-render",
|
|
65
65
|
"render": {
|
|
66
|
-
"mo": { "nodeId": "...", "file": "mo-main/content/hero-title.
|
|
67
|
-
"pc": { "nodeId": "...", "file": "pc-main/content/hero-title.
|
|
66
|
+
"mo": { "nodeId": "...", "file": "mo-main/content/hero-title.webp", "size": { "w": 620, "h": 174 } },
|
|
67
|
+
"pc": { "nodeId": "...", "file": "pc-main/content/hero-title.webp", "size": { "w": 1200, "h": 340 } }
|
|
68
68
|
},
|
|
69
69
|
"alt": "추운 겨울, 따뜻한 보상이 펑펑"
|
|
70
70
|
},
|
|
@@ -72,8 +72,8 @@ tree.json을 코드 생성에 최적화된 구조로 리매핑한다.
|
|
|
72
72
|
"role": "subtitle-image",
|
|
73
73
|
"type": "node-render",
|
|
74
74
|
"render": {
|
|
75
|
-
"mo": { "nodeId": "...", "file": "mo-main/content/hero-subtitle.
|
|
76
|
-
"pc": { "nodeId": "...", "file": "pc-main/content/hero-subtitle.
|
|
75
|
+
"mo": { "nodeId": "...", "file": "mo-main/content/hero-subtitle.webp", "size": { "w": 586, "h": 32 } },
|
|
76
|
+
"pc": { "nodeId": "...", "file": "pc-main/content/hero-subtitle.webp", "size": { "w": 1100, "h": 60 } }
|
|
77
77
|
},
|
|
78
78
|
"alt": "겨울을 녹일 보상, 지금 PC방에서 획득하세요!"
|
|
79
79
|
}
|
|
@@ -93,8 +93,8 @@ tree.json을 코드 생성에 최적화된 구조로 리매핑한다.
|
|
|
93
93
|
"role": "period-panel",
|
|
94
94
|
"type": "content-with-bg",
|
|
95
95
|
"bg": {
|
|
96
|
-
"mo": { "nodeId": "...", "file": "mo-main/bg/period-bg.
|
|
97
|
-
"pc": { "nodeId": "...", "file": "pc-main/bg/period-bg.
|
|
96
|
+
"mo": { "nodeId": "...", "file": "mo-main/bg/period-bg.webp" },
|
|
97
|
+
"pc": { "nodeId": "...", "file": "pc-main/bg/period-bg.webp" }
|
|
98
98
|
},
|
|
99
99
|
"size": {
|
|
100
100
|
"mo": { "w": 720, "h": 500 },
|
|
@@ -11,8 +11,15 @@ tier: standard
|
|
|
11
11
|
**Claude는 시맨틱 판단(태그 선택, 컴포넌트 분리, 인터랙션)만 담당한다.**
|
|
12
12
|
|
|
13
13
|
```
|
|
14
|
+
⛔ 불변 규칙 — 복잡한 UI도 반드시 HTML로 구현한다:
|
|
15
|
+
"복잡하니까 이미지로 처리하자" → 이 사고방식 자체가 금지.
|
|
16
|
+
카드 그리드, 보상 목록, 교환소, 가격 표시 → 전부 HTML+CSS.
|
|
17
|
+
<img>는 순수 이미지 에셋(아이콘, 썸네일, 벡터 글자)에만 사용.
|
|
18
|
+
BG는 CSS background-image만 사용. <img> 태그 금지.
|
|
19
|
+
|
|
14
20
|
❌ 스크린샷을 보고 CSS 추정 (범용 LLM의 약점)
|
|
15
21
|
❌ Figma 레이어를 무분별하게 div soup로 변환
|
|
22
|
+
❌ 복잡한 섹션을 screenshot으로 이미지화 (코드 생성 중 screenshot 호출 금지)
|
|
16
23
|
✅ Figma Auto Layout → CSS Flexbox 1:1 매핑 (기계적)
|
|
17
24
|
✅ Figma CSS 속성 → SCSS 직접 변환 (추정 없음)
|
|
18
25
|
✅ Claude → 시맨틱 태그 선택 + 컴포넌트 설계 + 인터랙션
|
|
@@ -55,11 +62,45 @@ component-index (/tmp/{feature}/component-index.json) 에서 매칭되는 컴포
|
|
|
55
62
|
### 1-1. 노드 → HTML 매핑 규칙 (기계적)
|
|
56
63
|
|
|
57
64
|
```
|
|
65
|
+
⛔ 코드 작성 전: 이미지 vs HTML 판별 테이블을 먼저 작성한다 (BLOCKING).
|
|
66
|
+
섹션의 모든 1~2depth 노드를 순회하여 각각의 처리 방법을 결정.
|
|
67
|
+
판별 없이 코드를 작성하면 안 된다.
|
|
68
|
+
|
|
69
|
+
판별 규칙 (순서대로 적용, 하나라도 YES → HTML):
|
|
70
|
+
Q1. 이 노드의 자식 트리에 TEXT 노드가 있는가?
|
|
71
|
+
YES → HTML로 구현 (이미지에 텍스트를 넣지 않는다)
|
|
72
|
+
※ "가격 1,000", "보상 교환하기", "이벤트 기간" 등이 있으면 무조건 HTML
|
|
73
|
+
Q2. 같은 부모 아래 동일 구조 INSTANCE가 2개 이상인가?
|
|
74
|
+
YES → HTML 반복 구조 (v-for/.map) — 내부 아이콘/썸네일만 <img>
|
|
75
|
+
※ 카드 4개 그리드를 이미지 1장으로 렌더링하면 절대 안 됨
|
|
76
|
+
Q3. 클릭/인터랙션이 필요한가? (btn, CTA, link, tab)
|
|
77
|
+
YES → HTML <button>/<a>
|
|
78
|
+
Q4. 동적으로 변경되는 데이터인가? (가격, 수량, 기간, 상태)
|
|
79
|
+
YES → HTML 텍스트
|
|
80
|
+
모두 NO → 이미지 렌더링 가능 (벡터 글자, 래스터 에셋, 합성 BG 등)
|
|
81
|
+
|
|
58
82
|
각 노드에 대해 아래 규칙을 순서대로 적용:
|
|
59
83
|
|
|
60
|
-
1.
|
|
84
|
+
1. 배경 레이어 (BG 프레임 — 가장 먼저 처리):
|
|
85
|
+
BG 프레임 (name에 "BG"/"bg" 또는 부모와 동일 크기)
|
|
86
|
+
|
|
87
|
+
❌ 절대 금지 패턴 (이렇게 쓰면 안 됨):
|
|
88
|
+
<img src="hero-bg.webp" class="bg-img" />
|
|
89
|
+
<div class="bg"><img src="bg.webp" /></div>
|
|
90
|
+
position: absolute; inset: 0; → 이미지 배치
|
|
91
|
+
|
|
92
|
+
✅ 유일하게 허용되는 패턴:
|
|
93
|
+
부모 요소의 SCSS에 background-image로 처리:
|
|
94
|
+
.heroSection {
|
|
95
|
+
background-image: url('/images/{feature}/hero-bg.webp');
|
|
96
|
+
background-size: cover;
|
|
97
|
+
background-position: center top;
|
|
98
|
+
}
|
|
99
|
+
HTML에는 BG 관련 요소를 아무것도 넣지 않음.
|
|
100
|
+
|
|
101
|
+
2. 타입별 기본 매핑:
|
|
61
102
|
TEXT 노드 → <span> (Claude가 <h1>~<h6>, <p>, <button> 등으로 승격)
|
|
62
|
-
IMAGE fill → <img src="다운로드된 경로" />
|
|
103
|
+
IMAGE fill → <img src="다운로드된 경로" /> (판별 통과한 순수 에셋만)
|
|
63
104
|
VECTOR/GROUP → 크기가 작으면(≤64px) 아이콘 후보 → <img> (렌더링 이미지)
|
|
64
105
|
크기가 크면 장식 요소 → <div> + background
|
|
65
106
|
FRAME/INSTANCE:
|
|
@@ -67,20 +108,21 @@ component-index (/tmp/{feature}/component-index.json) 에서 매칭되는 컴포
|
|
|
67
108
|
Auto Layout 없음 → <div> + position:relative (자식은 absolute)
|
|
68
109
|
children 없음 → 빈 div 또는 스킵
|
|
69
110
|
|
|
70
|
-
2. 배경 레이어 판별:
|
|
71
|
-
BG 프레임 (name에 "BG"/"bg" 또는 부모와 동일 크기)
|
|
72
|
-
❌ <img> 태그로 배경 처리 금지
|
|
73
|
-
❌ position:absolute + inset:0 으로 이미지 배치 금지
|
|
74
|
-
✅ 부모 요소에 CSS background-image로 처리:
|
|
75
|
-
background-image: url('/images/{feature}/{section}-bg.png');
|
|
76
|
-
background-size: cover;
|
|
77
|
-
background-position: center top;
|
|
78
|
-
✅ BG 프레임은 HTML에 아무것도 렌더링하지 않음 (CSS만)
|
|
79
|
-
|
|
80
111
|
3. 반복 패턴 감지:
|
|
81
|
-
같은 부모 아래 동일 타입 + 유사 구조(children 수 동일)
|
|
112
|
+
같은 부모 아래 동일 타입 + 유사 구조(children 수 동일) INSTANCE 2개 이상
|
|
82
113
|
→ v-for (Vue) 또는 .map() (React)
|
|
83
114
|
→ 첫 번째 노드를 기준으로 템플릿 생성
|
|
115
|
+
→ 카드 내부의 이미지 에셋(아이콘, 썸네일)만 <img>
|
|
116
|
+
→ 카드 레이아웃, 텍스트, 버튼, 가격은 HTML로 구현
|
|
117
|
+
|
|
118
|
+
❌ 잘못된 예: <img src="exchange-section1.webp" /> (카드 4개가 한 이미지)
|
|
119
|
+
✅ 올바른 예:
|
|
120
|
+
<div v-for="card in weeklyCards" :key="card.id" class="card">
|
|
121
|
+
<img :src="card.icon" :alt="card.name" class="card__icon" />
|
|
122
|
+
<span class="card__name">{{ card.name }}</span>
|
|
123
|
+
<span class="card__price">{{ card.price }}</span>
|
|
124
|
+
<button class="card__btn">보상 교환하기</button>
|
|
125
|
+
</div>
|
|
84
126
|
|
|
85
127
|
4. 스킵 대상:
|
|
86
128
|
크기 0px 노드
|
|
@@ -331,8 +373,8 @@ $bp-desktop: 1024px;
|
|
|
331
373
|
Hero (INSTANCE 720x1280)
|
|
332
374
|
├── BG (FRAME — 배경 레이어)
|
|
333
375
|
├── Title (FRAME — flex-column, gap:24px)
|
|
334
|
-
│ ├── Title (RECTANGLE — imageRef → title.
|
|
335
|
-
│ └── Sub Title (VECTOR — imageRef → subtitle.
|
|
376
|
+
│ ├── Title (RECTANGLE — imageRef → title.webp)
|
|
377
|
+
│ └── Sub Title (VECTOR — imageRef → subtitle.webp)
|
|
336
378
|
├── Period (FRAME — flex-column, gap:10px, padding)
|
|
337
379
|
│ └── Period (FRAME — flex-column, gap:22px)
|
|
338
380
|
│ ├── Period_Left (FRAME — flex-column, gap:4px)
|
|
@@ -346,14 +388,14 @@ $bp-desktop: 1024px;
|
|
|
346
388
|
<template>
|
|
347
389
|
<section class="heroSection">
|
|
348
390
|
<!-- BG: CSS background-image로 처리 (img 태그 아님!) -->
|
|
349
|
-
<!-- .heroSection { background-image: url('/images/{feature}/hero-bg.
|
|
391
|
+
<!-- .heroSection { background-image: url('/images/{feature}/hero-bg.webp'); background-size: cover; } -->
|
|
350
392
|
|
|
351
393
|
<!-- Title: flex-column, gap:24px → 직접 매핑 -->
|
|
352
394
|
<div class="heroTitle">
|
|
353
|
-
<img src="/images/{feature}/title.
|
|
395
|
+
<img src="/images/{feature}/title.webp"
|
|
354
396
|
alt="추운 겨울, 따뜻한 보상이 펑펑"
|
|
355
397
|
class="heroTitleImg" />
|
|
356
|
-
<img src="/images/{feature}/subtitle.
|
|
398
|
+
<img src="/images/{feature}/subtitle.webp"
|
|
357
399
|
alt="겨울을 녹일 보상, 지금 PC방에서 획득하세요!"
|
|
358
400
|
class="heroSubtitleImg" />
|
|
359
401
|
</div>
|
|
@@ -377,7 +419,7 @@ $bp-desktop: 1024px;
|
|
|
377
419
|
|
|
378
420
|
<!-- BTN_Share: flex, borderRadius:500px → 버튼으로 승격 -->
|
|
379
421
|
<button class="heroShareBtn" @click="handleShare">
|
|
380
|
-
<img src="/images/{feature}/share-icon.
|
|
422
|
+
<img src="/images/{feature}/share-icon.webp" alt="공유하기" class="heroShareIcon" />
|
|
381
423
|
</button>
|
|
382
424
|
</section>
|
|
383
425
|
</template>
|
|
@@ -466,7 +508,7 @@ function handleShare(): void {
|
|
|
466
508
|
같은 값 → 유지
|
|
467
509
|
다른 px 값 → @include pc { width: {desktop값 × pcScaleFactor}px; }
|
|
468
510
|
다른 레이아웃 → @include pc { flex-direction: row; }
|
|
469
|
-
다른 이미지 → @include pc { content: url(/images/{feature}/desktop-xxx.
|
|
511
|
+
다른 이미지 → @include pc { content: url(/images/{feature}/desktop-xxx.webp); }
|
|
470
512
|
|
|
471
513
|
기존 모바일 코드 삭제 금지.
|
|
472
514
|
```
|
|
@@ -21,9 +21,52 @@
|
|
|
21
21
|
|
|
22
22
|
## 배경 레이어 판별
|
|
23
23
|
|
|
24
|
-
부모와 동일 크기
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
BG 프레임 (name에 "BG"/"bg" 또는 부모와 동일 크기 ±5%):
|
|
25
|
+
❌ <img> 태그로 배경 처리 금지
|
|
26
|
+
❌ position:absolute + inset:0 으로 이미지 배치 금지
|
|
27
|
+
✅ 부모 요소에 CSS background-image로만 처리:
|
|
28
|
+
background-image: url('/images/{feature}/{section}-bg.webp');
|
|
29
|
+
background-size: cover;
|
|
30
|
+
background-position: center top;
|
|
31
|
+
✅ BG 프레임은 HTML에 아무것도 렌더링하지 않음 (CSS만)
|
|
32
|
+
|
|
33
|
+
## 이미지 vs HTML 판별 (각 노드마다 확인)
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
노드를 이미지로 처리하기 전에 반드시 확인:
|
|
37
|
+
|
|
38
|
+
Q1. 이 노드에 TEXT 자식이 있는가?
|
|
39
|
+
YES → HTML로 구현 (텍스트는 이미지에 넣지 않는다)
|
|
40
|
+
NO → Q2로
|
|
41
|
+
|
|
42
|
+
Q2. 이 노드가 INSTANCE 반복 패턴의 일부인가?
|
|
43
|
+
YES → HTML 반복 구조 (v-for/.map()) — 내부 이미지 에셋만 개별 추출
|
|
44
|
+
NO → Q3로
|
|
45
|
+
|
|
46
|
+
Q3. 이 노드에 클릭/인터랙션이 필요한가? (btn, CTA, link, tab)
|
|
47
|
+
YES → HTML <button>/<a> — 이미지 금지
|
|
48
|
+
NO → Q4로
|
|
49
|
+
|
|
50
|
+
Q4. 이 노드의 콘텐츠가 동적 데이터인가? (가격, 수량, 기간, 상태)
|
|
51
|
+
YES → HTML 텍스트 — 이미지 금지
|
|
52
|
+
NO → 이미지 렌더링 가능
|
|
53
|
+
|
|
54
|
+
위 체크에서 하나라도 YES → HTML로 구현
|
|
55
|
+
모두 NO → 이미지(벡터 글자, BG, 래스터, 복잡 그래픽)로 렌더링
|
|
56
|
+
|
|
57
|
+
실제 테스트에서 발생한 잘못된 패턴:
|
|
58
|
+
|
|
59
|
+
❌ <img src="hero-bg.webp" class="bg-img" /> → BG를 img 태그로
|
|
60
|
+
❌ <img src="exchange-section1.webp" /> → 카드 4개를 이미지 1장으로
|
|
61
|
+
❌ <img src="daily-step2-list.webp" /> → 보상 목록을 통째 이미지로
|
|
62
|
+
❌ <img src="hero-period-bg-mo.webp" class="period-bg"> → Period BG를 img로
|
|
63
|
+
|
|
64
|
+
✅ .heroSection { background-image: url('hero-bg.webp'); background-size: cover; }
|
|
65
|
+
✅ <div v-for="card in cards" :key="card.id">
|
|
66
|
+
<img :src="card.icon" /> <span>{{ card.price }}</span>
|
|
67
|
+
<button>보상 교환하기</button>
|
|
68
|
+
</div>
|
|
69
|
+
```
|
|
27
70
|
|
|
28
71
|
## CSS 직접 매핑 규칙
|
|
29
72
|
|
|
@@ -77,6 +120,9 @@ BEM 패턴: `.sectionName__childName`
|
|
|
77
120
|
|
|
78
121
|
## 자가 검증 (코드 작성 후)
|
|
79
122
|
|
|
123
|
+
- [ ] ⛔ BG 프레임이 <img> 태그로 처리되지 않았는가? (CSS background-image만 허용)
|
|
124
|
+
- [ ] ⛔ TEXT 자식이 있는 프레임이 통째 이미지로 처리되지 않았는가?
|
|
125
|
+
- [ ] ⛔ INSTANCE 반복 패턴(카드/아이템)이 이미지 1장으로 처리되지 않았는가?
|
|
80
126
|
- [ ] template 클래스 ↔ SCSS 클래스 1:1 일치
|
|
81
127
|
- [ ] 모든 img src가 static/에 실제 존재
|
|
82
128
|
- [ ] Auto Layout 노드 → SCSS에 flex 속성 존재
|
|
@@ -21,7 +21,7 @@ tree.json 노드를 기반으로 컴포넌트를 생성할 때 이 템플릿을
|
|
|
21
21
|
<template>
|
|
22
22
|
<section class="{{sectionName}}">
|
|
23
23
|
<!-- BG: CSS background-image로 처리. img 태그 사용 금지! -->
|
|
24
|
-
<!-- SCSS: .{{sectionName}} { background-image: url('/images/{{FEATURE_KEY}}/{{bg-file}}.
|
|
24
|
+
<!-- SCSS: .{{sectionName}} { background-image: url('/images/{{FEATURE_KEY}}/{{bg-file}}.webp'); background-size: cover; } -->
|
|
25
25
|
|
|
26
26
|
<!-- {{CHILD_1}}: tree flex-column, gap:{{GAP}}px → 직접 매핑 -->
|
|
27
27
|
<div class="{{sectionName}}__{{child1Name}}">
|
|
@@ -86,7 +86,7 @@ defineProps<{
|
|
|
86
86
|
width: 100%;
|
|
87
87
|
height: {{HEIGHT_VW}}vw; // tree: {{HEIGHT}} / {{DESIGN_WIDTH}} × 100
|
|
88
88
|
overflow: hidden; // tree: overflow:hidden
|
|
89
|
-
background-image: url('/images/{{FEATURE_KEY}}/{{section}}-bg.
|
|
89
|
+
background-image: url('/images/{{FEATURE_KEY}}/{{section}}-bg.webp');
|
|
90
90
|
background-size: cover;
|
|
91
91
|
background-position: center top;
|
|
92
92
|
}
|
|
@@ -9,6 +9,18 @@ tier: standard
|
|
|
9
9
|
|
|
10
10
|
Figma REST API(`src/infra/lib/figma/`)를 사용하여 **구조적 코드 생성에 필요한 모든 데이터**를 추출.
|
|
11
11
|
|
|
12
|
+
```
|
|
13
|
+
⛔ 불변 규칙 — screenshot 명령의 허용 범위:
|
|
14
|
+
✅ BG 프레임 렌더링 (TEXT 자식이 없는 순수 배경만)
|
|
15
|
+
✅ 벡터 글자 GROUP (웹폰트 없는 장식 타이틀)
|
|
16
|
+
✅ 개별 아이콘/썸네일 (50~300px 독립 에셋)
|
|
17
|
+
✅ 섹션별 전체 스크린샷 → sections/ (Phase 6 검증용)
|
|
18
|
+
❌ TEXT 자식이 있는 프레임 (가격, 수량, 설명 등)
|
|
19
|
+
❌ INSTANCE 반복 패턴 프레임 (카드 그리드, 보상 목록)
|
|
20
|
+
❌ 버튼/인터랙티브 요소가 있는 프레임
|
|
21
|
+
❌ "섹션 콘텐츠"를 통째로 렌더링
|
|
22
|
+
```
|
|
23
|
+
|
|
12
24
|
```
|
|
13
25
|
추출 우선순위:
|
|
14
26
|
1순위: 노드 트리 + CSS (코드 생성의 PRIMARY 소스)
|
|
@@ -101,35 +113,70 @@ Bash:
|
|
|
101
113
|
|
|
102
114
|
⚠️ 주의: BG 프레임만 렌더링한다 (텍스트 포함된 상위 프레임 렌더링 금지)
|
|
103
115
|
❌ 섹션 전체를 렌더링 → 텍스트가 이미지에 포함 → HTML 텍스트와 중복
|
|
116
|
+
❌ TEXT 자식이 있는 프레임을 렌더링 → 이미지 텍스트 + HTML 텍스트 이중 표시
|
|
104
117
|
✅ BG 하위 프레임만 렌더링 → 텍스트 없는 배경만 → CSS background-image
|
|
105
118
|
✅ 텍스트는 tree.json에서 추출하여 HTML로 작성
|
|
106
119
|
|
|
120
|
+
렌더링 전 TEXT 자식 검증 (BLOCKING):
|
|
121
|
+
BG 프레임의 전체 자식 트리를 순회하여 TEXT 노드 존재 여부 확인
|
|
122
|
+
TEXT 노드 발견 시:
|
|
123
|
+
→ TEXT 노드가 포함된 하위 프레임은 렌더링에서 제외
|
|
124
|
+
→ BG 프레임 내 순수 시각 요소(이미지, 벡터, 장식)만 렌더링
|
|
125
|
+
→ 또는 TEXT가 없는 가장 깊은 BG 하위 프레임을 개별 렌더링
|
|
126
|
+
|
|
107
127
|
BG 프레임 판별 기준:
|
|
108
128
|
- name에 "BG", "bg" 포함
|
|
109
129
|
- 또는 부모와 크기 동일(±10%) + 자식 이미지 3개 이상
|
|
110
130
|
- 또는 1depth 첫 번째 자식이면서 이미지 노드 다수 보유
|
|
111
131
|
|
|
112
132
|
렌더링:
|
|
113
|
-
node "[FIGMA_SCRIPT]" screenshot {fileKey} {bg.nodeId} --out=/tmp/{feature}/bg/{section}-bg.
|
|
133
|
+
node "[FIGMA_SCRIPT]" screenshot {fileKey} {bg.nodeId} --out=/tmp/{feature}/bg/{section}-bg.webp
|
|
114
134
|
|
|
115
135
|
→ BG 하위 20+ 레이어가 합성된 1장
|
|
116
136
|
→ CSS background-image로 처리
|
|
117
137
|
→ 개별 레이어(눈, 나무, 파티클 등) 다운로드하지 않음
|
|
118
138
|
```
|
|
119
139
|
|
|
140
|
+
### 2-1.5. 렌더링 금지 노드 (HTML로 구현할 것)
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
다음 조건에 해당하는 노드는 이미지로 렌더링하지 않는다:
|
|
144
|
+
|
|
145
|
+
1. TEXT 자식 보유 프레임 → HTML로 구현
|
|
146
|
+
- 기간, 가격, 수량, 설명 등 텍스트 → HTML
|
|
147
|
+
- "1,000", "보상 교환하기", "2025.12.22" 등 → 이미지에 넣지 않음
|
|
148
|
+
- 텍스트가 이미지에 포함되면 수정/번역/접근성 불가
|
|
149
|
+
|
|
150
|
+
2. INSTANCE 반복 패턴 (카드/아이템 그리드) → HTML 반복 구조
|
|
151
|
+
- 같은 부모 아래 동일 구조 INSTANCE 2개 이상
|
|
152
|
+
- ❌ 카드 그리드를 통째 이미지 1장으로 렌더링 금지
|
|
153
|
+
- ✅ 각 카드 내부의 이미지 에셋(아이콘, 썸네일)만 개별 추출
|
|
154
|
+
- ✅ 카드 레이아웃, 텍스트, 버튼은 HTML+CSS
|
|
155
|
+
|
|
156
|
+
3. 인터랙티브 요소 → HTML <button>/<a>
|
|
157
|
+
- name에 "btn", "button", "CTA", "link" 포함
|
|
158
|
+
- 클릭 이벤트가 필요한 요소는 이미지 금지
|
|
159
|
+
|
|
160
|
+
4. 정보 표시 영역 → HTML 텍스트
|
|
161
|
+
- 기간 표시 ("이벤트 기간", "교환/응모 종료일")
|
|
162
|
+
- 가격/수량 ("1,000", "500")
|
|
163
|
+
- 상태 표시 ("참여 대상", "로그인")
|
|
164
|
+
```
|
|
165
|
+
|
|
120
166
|
### 2-2. 콘텐츠 노드 렌더링
|
|
121
167
|
|
|
122
168
|
```
|
|
123
169
|
BG가 아닌 콘텐츠 이미지를 개별 노드 렌더링:
|
|
124
170
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
171
|
+
⚠️ 렌더링 전 2-1.5 체크 필수 — 하나라도 해당하면 이미지 렌더링 금지
|
|
172
|
+
|
|
173
|
+
대상 식별 (tree.json에서 — TEXT 자식 미보유 노드만):
|
|
174
|
+
- 타이틀/서브타이틀 이미지 (name에 "title", "sub title") — 벡터 글자만, TEXT 노드 아님
|
|
128
175
|
- 아이콘 (VECTOR/GROUP 크기 ≤ 64px)
|
|
129
|
-
- 아이템/보상
|
|
176
|
+
- 아이템/보상 썸네일 (name에 "item", "reward", "token", "coin") — 이미지 에셋만
|
|
130
177
|
|
|
131
178
|
렌더링 (imageRef 다운로드 아님!):
|
|
132
|
-
node "[FIGMA_SCRIPT]" screenshot {fileKey} {node.nodeId} --out=/tmp/{feature}/content/{name}.
|
|
179
|
+
node "[FIGMA_SCRIPT]" screenshot {fileKey} {node.nodeId} --out=/tmp/{feature}/content/{name}.webp
|
|
133
180
|
|
|
134
181
|
→ 텍스처 fill이 적용된 최종 결과물이 나옴
|
|
135
182
|
→ 22.7MB 텍스처 대신 364KB 렌더링 이미지
|
|
@@ -147,7 +194,7 @@ BG가 아닌 콘텐츠 이미지를 개별 노드 렌더링:
|
|
|
147
194
|
|
|
148
195
|
렌더링:
|
|
149
196
|
부모 GROUP을 통째로 렌더링 (개별 글자 다운로드 금지)
|
|
150
|
-
node "[FIGMA_SCRIPT]" screenshot {fileKey} {group.nodeId} --out=/tmp/{feature}/content/{name}.
|
|
197
|
+
node "[FIGMA_SCRIPT]" screenshot {fileKey} {group.nodeId} --out=/tmp/{feature}/content/{name}.webp
|
|
151
198
|
|
|
152
199
|
예시:
|
|
153
200
|
"MISSION 01" GROUP (174x42, 벡터 9개) → 렌더링 1장 (58KB)
|
|
@@ -173,10 +220,10 @@ BG가 아닌 콘텐츠 이미지를 개별 노드 렌더링:
|
|
|
173
220
|
코드 생성에는 사용하지 않는다. Phase 4 시각 검증에서만 사용.
|
|
174
221
|
|
|
175
222
|
전체 스크린샷:
|
|
176
|
-
node "[FIGMA_SCRIPT]" screenshot {fileKey} {nodeId} --out=/tmp/{feature}/full-screenshot.
|
|
223
|
+
node "[FIGMA_SCRIPT]" screenshot {fileKey} {nodeId} --out=/tmp/{feature}/full-screenshot.webp
|
|
177
224
|
|
|
178
225
|
섹션별 스크린샷 (1depth 자식 프레임 각각):
|
|
179
|
-
node "[FIGMA_SCRIPT]" screenshot {fileKey} {child.nodeId} --out=/tmp/{feature}/sections/{name}.
|
|
226
|
+
node "[FIGMA_SCRIPT]" screenshot {fileKey} {child.nodeId} --out=/tmp/{feature}/sections/{name}.webp
|
|
180
227
|
|
|
181
228
|
용도:
|
|
182
229
|
✅ Phase 4에서 렌더링 결과와 pixelmatch 비교
|
|
@@ -194,26 +241,26 @@ BG가 아닌 콘텐츠 이미지를 개별 노드 렌더링:
|
|
|
194
241
|
/tmp/{feature}/
|
|
195
242
|
├── tree.json ← 코드 생성의 PRIMARY 소스
|
|
196
243
|
├── bg/ ← BG 프레임 렌더링 (섹션당 1장)
|
|
197
|
-
│ ├── hero-bg.
|
|
198
|
-
│ ├── daily-bg.
|
|
244
|
+
│ ├── hero-bg.webp
|
|
245
|
+
│ ├── daily-bg.webp
|
|
199
246
|
│ └── ...
|
|
200
247
|
├── content/ ← 콘텐츠 노드 렌더링
|
|
201
|
-
│ ├── hero-title.
|
|
202
|
-
│ ├── hero-subtitle.
|
|
203
|
-
│ ├── mission-01.
|
|
204
|
-
│ ├── btn-login.
|
|
248
|
+
│ ├── hero-title.webp
|
|
249
|
+
│ ├── hero-subtitle.webp
|
|
250
|
+
│ ├── mission-01.webp ← 벡터 글자 그룹 렌더링
|
|
251
|
+
│ ├── btn-login.webp
|
|
205
252
|
│ └── ...
|
|
206
|
-
├── full-screenshot.
|
|
253
|
+
├── full-screenshot.webp ← Phase 4 검증용
|
|
207
254
|
└── sections/ ← Phase 4 섹션별 검증용
|
|
208
|
-
├── hero.
|
|
255
|
+
├── hero.webp
|
|
209
256
|
└── ...
|
|
210
257
|
|
|
211
258
|
이미지 분류 (실제 테스트 기준):
|
|
212
259
|
| 분류 | 처리 | 예시 |
|
|
213
260
|
|------|------|------|
|
|
214
|
-
| BG 프레임 (89개) | 프레임 렌더링 → bg/ | hero-bg.
|
|
215
|
-
| 벡터 글자 (33개) | GROUP 렌더링 → content/ | mission-01.
|
|
216
|
-
| 콘텐츠 (8개) | 노드 렌더링 → content/ | hero-title.
|
|
261
|
+
| BG 프레임 (89개) | 프레임 렌더링 → bg/ | hero-bg.webp (4.2MB) |
|
|
262
|
+
| 벡터 글자 (33개) | GROUP 렌더링 → content/ | mission-01.webp (58KB) |
|
|
263
|
+
| 콘텐츠 (8개) | 노드 렌더링 → content/ | hero-title.webp (364KB) |
|
|
217
264
|
| 장식 (29개) | BG 렌더링에 포함 | — |
|
|
218
265
|
→ 전체 159개 → 실제 파일 약 18장
|
|
219
266
|
|
|
@@ -4,16 +4,24 @@
|
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
❌ imageRef 개별 다운로드 금지 (텍스처 fill 공유 문제)
|
|
7
|
+
❌ 섹션/그룹을 통째 이미지로 렌더링 금지 (내부 TEXT/INSTANCE 포함 시)
|
|
7
8
|
✅ 모든 이미지는 Figma screenshot API로 노드 렌더링
|
|
9
|
+
✅ 렌더링 전 반드시 2-1.5 판별 규칙 확인 → HTML 대상은 렌더링하지 않음
|
|
10
|
+
|
|
11
|
+
실제 테스트에서 발생한 잘못된 렌더링:
|
|
12
|
+
❌ exchange-section1.webp (카드 4개 그리드를 이미지 1장으로)
|
|
13
|
+
❌ daily-step2-list.webp (보상 목록을 텍스트 포함하여 통째로)
|
|
14
|
+
❌ prize-section1.webp (응모 아이템을 텍스트 포함하여 통째로)
|
|
15
|
+
✅ 올바른 접근: 카드 내부의 아이콘/썸네일만 개별 렌더링
|
|
8
16
|
```
|
|
9
17
|
|
|
10
18
|
## Render Methods
|
|
11
19
|
|
|
12
20
|
| 이미지 유형 | 렌더링 방법 | 출력 위치 |
|
|
13
21
|
|-----------|-----------|---------|
|
|
14
|
-
| BG 프레임 (합성 배경) | `screenshot {fileKey} {bg.nodeId}` | `bg/{section}-bg.
|
|
15
|
-
| 콘텐츠 (타이틀, 버튼) | `screenshot {fileKey} {node.nodeId}` | `content/{name}.
|
|
16
|
-
| 벡터 글자 그룹 | `screenshot {fileKey} {group.nodeId}` | `content/{name}.
|
|
22
|
+
| BG 프레임 (합성 배경) | `screenshot {fileKey} {bg.nodeId}` | `bg/{section}-bg.webp` |
|
|
23
|
+
| 콘텐츠 (타이틀, 버튼) | `screenshot {fileKey} {node.nodeId}` | `content/{name}.webp` |
|
|
24
|
+
| 벡터 글자 그룹 | `screenshot {fileKey} {group.nodeId}` | `content/{name}.webp` |
|
|
17
25
|
|
|
18
26
|
## BG 프레임 판별
|
|
19
27
|
|
|
@@ -39,18 +47,66 @@ BG 프레임 = 다음 중 하나:
|
|
|
39
47
|
→ 커스텀 폰트 텍스트 = 웹폰트 없음 → 이미지로 사용
|
|
40
48
|
```
|
|
41
49
|
|
|
50
|
+
## 렌더링 금지 노드 (HTML로 구현해야 하는 것)
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
다음 조건에 해당하는 노드는 이미지로 렌더링하지 않는다 — HTML+CSS로 구현:
|
|
54
|
+
|
|
55
|
+
1. TEXT 자식 보유 프레임:
|
|
56
|
+
프레임 내부에 TEXT 노드가 1개 이상 있으면 → HTML로 구현
|
|
57
|
+
⚠️ BG 프레임 렌더링 시에도 TEXT 포함 여부 반드시 확인:
|
|
58
|
+
- BG 프레임에 TEXT 자식이 있으면 → BG 하위만 렌더링 (TEXT 제외)
|
|
59
|
+
- 텍스트 포함된 상위 프레임을 통째 렌더링하면 → 이미지 텍스트 + HTML 텍스트 이중 표시
|
|
60
|
+
|
|
61
|
+
2. INSTANCE 반복 패턴 (카드/아이템 그리드):
|
|
62
|
+
같은 부모 아래 동일 구조 INSTANCE 2개 이상 → HTML 반복 구조 (v-for/.map())
|
|
63
|
+
❌ 카드 그리드를 통째 이미지 1장으로 렌더링 금지
|
|
64
|
+
✅ 각 카드 내부의 이미지 에셋만 개별 추출 (아이콘, 아이템 이미지)
|
|
65
|
+
✅ 카드 레이아웃, 텍스트, 버튼은 HTML로 구현
|
|
66
|
+
|
|
67
|
+
3. 인터랙티브 요소:
|
|
68
|
+
name에 "btn", "button", "CTA", "link", "tab", "toggle" 포함 → HTML <button>/<a>
|
|
69
|
+
❌ 버튼을 이미지로 렌더링 금지 (클릭 이벤트 불가)
|
|
70
|
+
|
|
71
|
+
4. 정보 텍스트 영역:
|
|
72
|
+
기간, 가격, 수량, 설명 등 변경 가능 데이터 → HTML 텍스트
|
|
73
|
+
❌ "1,000", "500 G-COIN", "보상 교환하기" 등을 이미지에 포함 금지
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 이미지로 렌더링하는 것 (HTML로 구현 불가)
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
다음만 이미지로 렌더링:
|
|
80
|
+
|
|
81
|
+
1. 커스텀 폰트 텍스트 (벡터 글자 그룹):
|
|
82
|
+
- 웹폰트 없는 장식 타이틀 ("MISSION 01" 등)
|
|
83
|
+
- VECTOR 타입으로 분해된 글자 → GROUP 렌더링
|
|
84
|
+
|
|
85
|
+
2. 합성 배경 (BG 프레임):
|
|
86
|
+
- 눈, 나무, 파티클 등 장식 레이어 합성물
|
|
87
|
+
- 텍스트 미포함 확인 필수
|
|
88
|
+
|
|
89
|
+
3. 래스터 이미지 에셋:
|
|
90
|
+
- 게임 아이템 썸네일, 코인 아이콘 등
|
|
91
|
+
- imageRef가 있는 개별 RECTANGLE/노드
|
|
92
|
+
|
|
93
|
+
4. 복잡한 벡터 그래픽:
|
|
94
|
+
- CSS로 재현 불가능한 일러스트/아이콘
|
|
95
|
+
- VECTOR/GROUP 조합의 복잡한 그래픽
|
|
96
|
+
```
|
|
97
|
+
|
|
42
98
|
## Format
|
|
43
99
|
|
|
44
|
-
- Output format: `.
|
|
45
|
-
-
|
|
100
|
+
- Output format: `.webp` (Figma API에서 png 수신 → cwebp로 webp 변환)
|
|
101
|
+
- cwebp 미설치 시 `.png` 폴백
|
|
46
102
|
|
|
47
103
|
## Naming
|
|
48
104
|
|
|
49
105
|
렌더링된 이미지의 파일명 = Figma 노드 name을 kebab-case로:
|
|
50
|
-
- `"Hero"` BG frame → `hero-bg.
|
|
51
|
-
- `"Mission 01"` vector group → `mission-01.
|
|
52
|
-
- `"Title"` content → `hero-title.
|
|
53
|
-
- `"Btn_Login"` → `btn-login.
|
|
106
|
+
- `"Hero"` BG frame → `hero-bg.webp`
|
|
107
|
+
- `"Mission 01"` vector group → `mission-01.webp`
|
|
108
|
+
- `"Title"` content → `hero-title.webp` (섹션명 prefix)
|
|
109
|
+
- `"Btn_Login"` → `btn-login.webp`
|
|
54
110
|
|
|
55
111
|
Rules:
|
|
56
112
|
- 공백 → 하이픈
|