@su-record/vibe 2.8.46 → 2.8.47
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 +26 -20
- 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 +8 -8
- package/skills/vibe.figma.convert/rubrics/conversion-rules.md +33 -3
- package/skills/vibe.figma.convert/templates/component.md +2 -2
- package/skills/vibe.figma.extract/SKILL.md +55 -20
- package/skills/vibe.figma.extract/rubrics/image-rules.md +57 -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
|
@@ -355,7 +355,7 @@ URL 개수에 따른 처리:
|
|
|
355
355
|
URL에서 fileKey, nodeId 추출
|
|
356
356
|
|
|
357
357
|
1단계 — 전체 스크린샷 (정답 사진):
|
|
358
|
-
node "[FIGMA_SCRIPT]" screenshot {fileKey} {nodeId} --out=/tmp/{feature}/full-screenshot.
|
|
358
|
+
node "[FIGMA_SCRIPT]" screenshot {fileKey} {nodeId} --out=/tmp/{feature}/full-screenshot.webp
|
|
359
359
|
|
|
360
360
|
2단계 — 전체 트리 + CSS (수치 재료):
|
|
361
361
|
node "[FIGMA_SCRIPT]" tree {fileKey} {nodeId} --depth=10
|
|
@@ -377,7 +377,7 @@ URL에서 fileKey, nodeId 추출
|
|
|
377
377
|
|
|
378
378
|
4단계 — 섹션별 스크린샷 (부분 정답):
|
|
379
379
|
트리의 1depth 자식 프레임 각각:
|
|
380
|
-
node "[FIGMA_SCRIPT]" screenshot {fileKey} {child.nodeId} --out=/tmp/{feature}/sections/{child.name}.
|
|
380
|
+
node "[FIGMA_SCRIPT]" screenshot {fileKey} {child.nodeId} --out=/tmp/{feature}/sections/{child.name}.webp
|
|
381
381
|
```
|
|
382
382
|
|
|
383
383
|
### 2-1-multi. 멀티 프레임 재료 확보 (2개 이상 URL)
|
|
@@ -399,7 +399,7 @@ URL에서 fileKey, nodeId 추출
|
|
|
399
399
|
결과:
|
|
400
400
|
/tmp/{feature}/
|
|
401
401
|
├── frame-1/ ← 메인 페이지
|
|
402
|
-
│ ├── full-screenshot.
|
|
402
|
+
│ ├── full-screenshot.webp
|
|
403
403
|
│ ├── tree.json
|
|
404
404
|
│ ├── images/
|
|
405
405
|
│ └── sections/
|
|
@@ -417,17 +417,17 @@ Phase 2 완료 시 /tmp/{feature}/ 에 다음이 준비되어야 함:
|
|
|
417
417
|
|
|
418
418
|
단일 URL:
|
|
419
419
|
/tmp/{feature}/
|
|
420
|
-
├── full-screenshot.
|
|
420
|
+
├── full-screenshot.webp ← 전체 정답 사진
|
|
421
421
|
├── tree.json ← 노드 트리 + CSS 수치
|
|
422
422
|
├── images/ ← 모든 이미지 에셋
|
|
423
|
-
│ ├── hero-bg.
|
|
424
|
-
│ ├── hero-title.
|
|
425
|
-
│ ├── card-item-1.
|
|
423
|
+
│ ├── hero-bg.webp
|
|
424
|
+
│ ├── hero-title.webp
|
|
425
|
+
│ ├── card-item-1.webp
|
|
426
426
|
│ └── ...
|
|
427
427
|
└── sections/ ← 섹션별 정답 사진
|
|
428
|
-
├── hero.
|
|
429
|
-
├── daily-checkin.
|
|
430
|
-
├── playtime-mission.
|
|
428
|
+
├── hero.webp
|
|
429
|
+
├── daily-checkin.webp
|
|
430
|
+
├── playtime-mission.webp
|
|
431
431
|
└── ...
|
|
432
432
|
|
|
433
433
|
멀티 URL: 위 2-1-multi 결과 구조 참조
|
|
@@ -471,7 +471,7 @@ UI 요소 → vw 비례:
|
|
|
471
471
|
|
|
472
472
|
```
|
|
473
473
|
1. 시각 비교 — 각 프레임의 스크린샷을 순차 Read:
|
|
474
|
-
- frame-1/full-screenshot.
|
|
474
|
+
- frame-1/full-screenshot.webp → frame-2/full-screenshot.webp → ...
|
|
475
475
|
- 시각적으로 반복되는 요소 식별:
|
|
476
476
|
상단 영역 (GNB/Header), 하단 영역 (Footer),
|
|
477
477
|
카드 패턴, 버튼 스타일, 섹션 레이아웃
|
|
@@ -661,14 +661,20 @@ Phase 1에서 생성한 빈 SCSS 파일에 기본 내용 Write:
|
|
|
661
661
|
→ 스크린샷은 검증용으로만 참조
|
|
662
662
|
|
|
663
663
|
2. 기계적 매핑 (추정 없음):
|
|
664
|
-
a.
|
|
665
|
-
|
|
664
|
+
a. 이미지 vs HTML 판별 (각 노드마다 — vibe.figma.convert 체크리스트 참조):
|
|
665
|
+
- TEXT 자식 있음 → HTML (이미지에 텍스트 포함 금지)
|
|
666
|
+
- INSTANCE 반복 패턴 → HTML 반복 구조 (내부 이미지 에셋만 추출)
|
|
667
|
+
- 인터랙티브 요소 (btn, CTA) → HTML <button>
|
|
668
|
+
- 동적 데이터 (가격, 기간, 수량) → HTML 텍스트
|
|
669
|
+
- 위 모두 아님 → 이미지 렌더링 가능
|
|
670
|
+
b. 이미지: 판별 통과한 노드만 static/images/{feature}/에 배치
|
|
671
|
+
c. 노드 → HTML 매핑:
|
|
666
672
|
- BG 프레임 → CSS background-image (img 태그 아님)
|
|
667
673
|
- Auto Layout 있음 → <div> + flex (direction/gap/padding 직접)
|
|
668
674
|
- Auto Layout 없음 → <div> + position:relative (자식 absolute)
|
|
669
675
|
- TEXT 노드 → <span> (Claude가 h2/p/button으로 승격)
|
|
670
|
-
-
|
|
671
|
-
- 반복 패턴 (동일 구조
|
|
676
|
+
- 순수 이미지 에셋 → <img src="렌더링된 파일">
|
|
677
|
+
- 반복 패턴 (동일 구조 2+) → v-for (카드 내 이미지만 <img>, 나머지 HTML)
|
|
672
678
|
c. CSS 직접 매핑:
|
|
673
679
|
- node.css의 모든 속성을 SCSS에 1:1 매핑
|
|
674
680
|
- vw/clamp 반응형 단위 변환 (vibe.figma.convert 참조)
|
|
@@ -696,7 +702,7 @@ Phase 1에서 생성한 빈 SCSS 파일에 기본 내용 Write:
|
|
|
696
702
|
tree.json의 Auto Layout 구조를 HTML flex 레이아웃으로 직접 매핑.
|
|
697
703
|
Claude가 시맨틱 태그로 승격 (div → section/h2/p/button).
|
|
698
704
|
Phase 1의 기능 요소(v-for, @click, v-if) 보존.
|
|
699
|
-
이미지 경로: /images/{feature}/파일명.
|
|
705
|
+
이미지 경로: /images/{feature}/파일명.webp (실제 파일 존재 확인)
|
|
700
706
|
텍스트: tree.json의 TEXT 노드 characters 값 그대로.
|
|
701
707
|
|
|
702
708
|
<script setup>
|
|
@@ -870,15 +876,15 @@ import { captureScreenshot, compareScreenshots } from 'src/infra/lib/browser'
|
|
|
870
876
|
각 섹션에 대해:
|
|
871
877
|
1. 렌더링 결과 스크린샷 캡처:
|
|
872
878
|
await captureScreenshot(page, {
|
|
873
|
-
outPath: '/tmp/{feature}/rendered-{section}.
|
|
879
|
+
outPath: '/tmp/{feature}/rendered-{section}.webp',
|
|
874
880
|
selector: '.{section}Section', // Phase 1에서 만든 클래스
|
|
875
881
|
})
|
|
876
882
|
|
|
877
883
|
2. Figma 원본과 픽셀 비교:
|
|
878
884
|
const diff = await compareScreenshots(
|
|
879
|
-
'/tmp/{feature}/sections/{section}.
|
|
880
|
-
'/tmp/{feature}/rendered-{section}.
|
|
881
|
-
'/tmp/{feature}/diff-{section}.
|
|
885
|
+
'/tmp/{feature}/sections/{section}.webp', // Figma 원본
|
|
886
|
+
'/tmp/{feature}/rendered-{section}.webp', // 렌더링 결과
|
|
887
|
+
'/tmp/{feature}/diff-{section}.webp', // 차이 시각화
|
|
882
888
|
)
|
|
883
889
|
|
|
884
890
|
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 },
|
|
@@ -72,7 +72,7 @@ component-index (/tmp/{feature}/component-index.json) 에서 매칭되는 컴포
|
|
|
72
72
|
❌ <img> 태그로 배경 처리 금지
|
|
73
73
|
❌ position:absolute + inset:0 으로 이미지 배치 금지
|
|
74
74
|
✅ 부모 요소에 CSS background-image로 처리:
|
|
75
|
-
background-image: url('/images/{feature}/{section}-bg.
|
|
75
|
+
background-image: url('/images/{feature}/{section}-bg.webp');
|
|
76
76
|
background-size: cover;
|
|
77
77
|
background-position: center top;
|
|
78
78
|
✅ BG 프레임은 HTML에 아무것도 렌더링하지 않음 (CSS만)
|
|
@@ -331,8 +331,8 @@ $bp-desktop: 1024px;
|
|
|
331
331
|
Hero (INSTANCE 720x1280)
|
|
332
332
|
├── BG (FRAME — 배경 레이어)
|
|
333
333
|
├── Title (FRAME — flex-column, gap:24px)
|
|
334
|
-
│ ├── Title (RECTANGLE — imageRef → title.
|
|
335
|
-
│ └── Sub Title (VECTOR — imageRef → subtitle.
|
|
334
|
+
│ ├── Title (RECTANGLE — imageRef → title.webp)
|
|
335
|
+
│ └── Sub Title (VECTOR — imageRef → subtitle.webp)
|
|
336
336
|
├── Period (FRAME — flex-column, gap:10px, padding)
|
|
337
337
|
│ └── Period (FRAME — flex-column, gap:22px)
|
|
338
338
|
│ ├── Period_Left (FRAME — flex-column, gap:4px)
|
|
@@ -346,14 +346,14 @@ $bp-desktop: 1024px;
|
|
|
346
346
|
<template>
|
|
347
347
|
<section class="heroSection">
|
|
348
348
|
<!-- BG: CSS background-image로 처리 (img 태그 아님!) -->
|
|
349
|
-
<!-- .heroSection { background-image: url('/images/{feature}/hero-bg.
|
|
349
|
+
<!-- .heroSection { background-image: url('/images/{feature}/hero-bg.webp'); background-size: cover; } -->
|
|
350
350
|
|
|
351
351
|
<!-- Title: flex-column, gap:24px → 직접 매핑 -->
|
|
352
352
|
<div class="heroTitle">
|
|
353
|
-
<img src="/images/{feature}/title.
|
|
353
|
+
<img src="/images/{feature}/title.webp"
|
|
354
354
|
alt="추운 겨울, 따뜻한 보상이 펑펑"
|
|
355
355
|
class="heroTitleImg" />
|
|
356
|
-
<img src="/images/{feature}/subtitle.
|
|
356
|
+
<img src="/images/{feature}/subtitle.webp"
|
|
357
357
|
alt="겨울을 녹일 보상, 지금 PC방에서 획득하세요!"
|
|
358
358
|
class="heroSubtitleImg" />
|
|
359
359
|
</div>
|
|
@@ -377,7 +377,7 @@ $bp-desktop: 1024px;
|
|
|
377
377
|
|
|
378
378
|
<!-- BTN_Share: flex, borderRadius:500px → 버튼으로 승격 -->
|
|
379
379
|
<button class="heroShareBtn" @click="handleShare">
|
|
380
|
-
<img src="/images/{feature}/share-icon.
|
|
380
|
+
<img src="/images/{feature}/share-icon.webp" alt="공유하기" class="heroShareIcon" />
|
|
381
381
|
</button>
|
|
382
382
|
</section>
|
|
383
383
|
</template>
|
|
@@ -466,7 +466,7 @@ function handleShare(): void {
|
|
|
466
466
|
같은 값 → 유지
|
|
467
467
|
다른 px 값 → @include pc { width: {desktop값 × pcScaleFactor}px; }
|
|
468
468
|
다른 레이아웃 → @include pc { flex-direction: row; }
|
|
469
|
-
다른 이미지 → @include pc { content: url(/images/{feature}/desktop-xxx.
|
|
469
|
+
다른 이미지 → @include pc { content: url(/images/{feature}/desktop-xxx.webp); }
|
|
470
470
|
|
|
471
471
|
기존 모바일 코드 삭제 금지.
|
|
472
472
|
```
|
|
@@ -21,9 +21,39 @@
|
|
|
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
|
+
```
|
|
27
57
|
|
|
28
58
|
## CSS 직접 매핑 규칙
|
|
29
59
|
|
|
@@ -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
|
}
|
|
@@ -101,35 +101,70 @@ Bash:
|
|
|
101
101
|
|
|
102
102
|
⚠️ 주의: BG 프레임만 렌더링한다 (텍스트 포함된 상위 프레임 렌더링 금지)
|
|
103
103
|
❌ 섹션 전체를 렌더링 → 텍스트가 이미지에 포함 → HTML 텍스트와 중복
|
|
104
|
+
❌ TEXT 자식이 있는 프레임을 렌더링 → 이미지 텍스트 + HTML 텍스트 이중 표시
|
|
104
105
|
✅ BG 하위 프레임만 렌더링 → 텍스트 없는 배경만 → CSS background-image
|
|
105
106
|
✅ 텍스트는 tree.json에서 추출하여 HTML로 작성
|
|
106
107
|
|
|
108
|
+
렌더링 전 TEXT 자식 검증 (BLOCKING):
|
|
109
|
+
BG 프레임의 전체 자식 트리를 순회하여 TEXT 노드 존재 여부 확인
|
|
110
|
+
TEXT 노드 발견 시:
|
|
111
|
+
→ TEXT 노드가 포함된 하위 프레임은 렌더링에서 제외
|
|
112
|
+
→ BG 프레임 내 순수 시각 요소(이미지, 벡터, 장식)만 렌더링
|
|
113
|
+
→ 또는 TEXT가 없는 가장 깊은 BG 하위 프레임을 개별 렌더링
|
|
114
|
+
|
|
107
115
|
BG 프레임 판별 기준:
|
|
108
116
|
- name에 "BG", "bg" 포함
|
|
109
117
|
- 또는 부모와 크기 동일(±10%) + 자식 이미지 3개 이상
|
|
110
118
|
- 또는 1depth 첫 번째 자식이면서 이미지 노드 다수 보유
|
|
111
119
|
|
|
112
120
|
렌더링:
|
|
113
|
-
node "[FIGMA_SCRIPT]" screenshot {fileKey} {bg.nodeId} --out=/tmp/{feature}/bg/{section}-bg.
|
|
121
|
+
node "[FIGMA_SCRIPT]" screenshot {fileKey} {bg.nodeId} --out=/tmp/{feature}/bg/{section}-bg.webp
|
|
114
122
|
|
|
115
123
|
→ BG 하위 20+ 레이어가 합성된 1장
|
|
116
124
|
→ CSS background-image로 처리
|
|
117
125
|
→ 개별 레이어(눈, 나무, 파티클 등) 다운로드하지 않음
|
|
118
126
|
```
|
|
119
127
|
|
|
128
|
+
### 2-1.5. 렌더링 금지 노드 (HTML로 구현할 것)
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
다음 조건에 해당하는 노드는 이미지로 렌더링하지 않는다:
|
|
132
|
+
|
|
133
|
+
1. TEXT 자식 보유 프레임 → HTML로 구현
|
|
134
|
+
- 기간, 가격, 수량, 설명 등 텍스트 → HTML
|
|
135
|
+
- "1,000", "보상 교환하기", "2025.12.22" 등 → 이미지에 넣지 않음
|
|
136
|
+
- 텍스트가 이미지에 포함되면 수정/번역/접근성 불가
|
|
137
|
+
|
|
138
|
+
2. INSTANCE 반복 패턴 (카드/아이템 그리드) → HTML 반복 구조
|
|
139
|
+
- 같은 부모 아래 동일 구조 INSTANCE 2개 이상
|
|
140
|
+
- ❌ 카드 그리드를 통째 이미지 1장으로 렌더링 금지
|
|
141
|
+
- ✅ 각 카드 내부의 이미지 에셋(아이콘, 썸네일)만 개별 추출
|
|
142
|
+
- ✅ 카드 레이아웃, 텍스트, 버튼은 HTML+CSS
|
|
143
|
+
|
|
144
|
+
3. 인터랙티브 요소 → HTML <button>/<a>
|
|
145
|
+
- name에 "btn", "button", "CTA", "link" 포함
|
|
146
|
+
- 클릭 이벤트가 필요한 요소는 이미지 금지
|
|
147
|
+
|
|
148
|
+
4. 정보 표시 영역 → HTML 텍스트
|
|
149
|
+
- 기간 표시 ("이벤트 기간", "교환/응모 종료일")
|
|
150
|
+
- 가격/수량 ("1,000", "500")
|
|
151
|
+
- 상태 표시 ("참여 대상", "로그인")
|
|
152
|
+
```
|
|
153
|
+
|
|
120
154
|
### 2-2. 콘텐츠 노드 렌더링
|
|
121
155
|
|
|
122
156
|
```
|
|
123
157
|
BG가 아닌 콘텐츠 이미지를 개별 노드 렌더링:
|
|
124
158
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
159
|
+
⚠️ 렌더링 전 2-1.5 체크 필수 — 하나라도 해당하면 이미지 렌더링 금지
|
|
160
|
+
|
|
161
|
+
대상 식별 (tree.json에서 — TEXT 자식 미보유 노드만):
|
|
162
|
+
- 타이틀/서브타이틀 이미지 (name에 "title", "sub title") — 벡터 글자만, TEXT 노드 아님
|
|
128
163
|
- 아이콘 (VECTOR/GROUP 크기 ≤ 64px)
|
|
129
|
-
- 아이템/보상
|
|
164
|
+
- 아이템/보상 썸네일 (name에 "item", "reward", "token", "coin") — 이미지 에셋만
|
|
130
165
|
|
|
131
166
|
렌더링 (imageRef 다운로드 아님!):
|
|
132
|
-
node "[FIGMA_SCRIPT]" screenshot {fileKey} {node.nodeId} --out=/tmp/{feature}/content/{name}.
|
|
167
|
+
node "[FIGMA_SCRIPT]" screenshot {fileKey} {node.nodeId} --out=/tmp/{feature}/content/{name}.webp
|
|
133
168
|
|
|
134
169
|
→ 텍스처 fill이 적용된 최종 결과물이 나옴
|
|
135
170
|
→ 22.7MB 텍스처 대신 364KB 렌더링 이미지
|
|
@@ -147,7 +182,7 @@ BG가 아닌 콘텐츠 이미지를 개별 노드 렌더링:
|
|
|
147
182
|
|
|
148
183
|
렌더링:
|
|
149
184
|
부모 GROUP을 통째로 렌더링 (개별 글자 다운로드 금지)
|
|
150
|
-
node "[FIGMA_SCRIPT]" screenshot {fileKey} {group.nodeId} --out=/tmp/{feature}/content/{name}.
|
|
185
|
+
node "[FIGMA_SCRIPT]" screenshot {fileKey} {group.nodeId} --out=/tmp/{feature}/content/{name}.webp
|
|
151
186
|
|
|
152
187
|
예시:
|
|
153
188
|
"MISSION 01" GROUP (174x42, 벡터 9개) → 렌더링 1장 (58KB)
|
|
@@ -173,10 +208,10 @@ BG가 아닌 콘텐츠 이미지를 개별 노드 렌더링:
|
|
|
173
208
|
코드 생성에는 사용하지 않는다. Phase 4 시각 검증에서만 사용.
|
|
174
209
|
|
|
175
210
|
전체 스크린샷:
|
|
176
|
-
node "[FIGMA_SCRIPT]" screenshot {fileKey} {nodeId} --out=/tmp/{feature}/full-screenshot.
|
|
211
|
+
node "[FIGMA_SCRIPT]" screenshot {fileKey} {nodeId} --out=/tmp/{feature}/full-screenshot.webp
|
|
177
212
|
|
|
178
213
|
섹션별 스크린샷 (1depth 자식 프레임 각각):
|
|
179
|
-
node "[FIGMA_SCRIPT]" screenshot {fileKey} {child.nodeId} --out=/tmp/{feature}/sections/{name}.
|
|
214
|
+
node "[FIGMA_SCRIPT]" screenshot {fileKey} {child.nodeId} --out=/tmp/{feature}/sections/{name}.webp
|
|
180
215
|
|
|
181
216
|
용도:
|
|
182
217
|
✅ Phase 4에서 렌더링 결과와 pixelmatch 비교
|
|
@@ -194,26 +229,26 @@ BG가 아닌 콘텐츠 이미지를 개별 노드 렌더링:
|
|
|
194
229
|
/tmp/{feature}/
|
|
195
230
|
├── tree.json ← 코드 생성의 PRIMARY 소스
|
|
196
231
|
├── bg/ ← BG 프레임 렌더링 (섹션당 1장)
|
|
197
|
-
│ ├── hero-bg.
|
|
198
|
-
│ ├── daily-bg.
|
|
232
|
+
│ ├── hero-bg.webp
|
|
233
|
+
│ ├── daily-bg.webp
|
|
199
234
|
│ └── ...
|
|
200
235
|
├── content/ ← 콘텐츠 노드 렌더링
|
|
201
|
-
│ ├── hero-title.
|
|
202
|
-
│ ├── hero-subtitle.
|
|
203
|
-
│ ├── mission-01.
|
|
204
|
-
│ ├── btn-login.
|
|
236
|
+
│ ├── hero-title.webp
|
|
237
|
+
│ ├── hero-subtitle.webp
|
|
238
|
+
│ ├── mission-01.webp ← 벡터 글자 그룹 렌더링
|
|
239
|
+
│ ├── btn-login.webp
|
|
205
240
|
│ └── ...
|
|
206
|
-
├── full-screenshot.
|
|
241
|
+
├── full-screenshot.webp ← Phase 4 검증용
|
|
207
242
|
└── sections/ ← Phase 4 섹션별 검증용
|
|
208
|
-
├── hero.
|
|
243
|
+
├── hero.webp
|
|
209
244
|
└── ...
|
|
210
245
|
|
|
211
246
|
이미지 분류 (실제 테스트 기준):
|
|
212
247
|
| 분류 | 처리 | 예시 |
|
|
213
248
|
|------|------|------|
|
|
214
|
-
| BG 프레임 (89개) | 프레임 렌더링 → bg/ | hero-bg.
|
|
215
|
-
| 벡터 글자 (33개) | GROUP 렌더링 → content/ | mission-01.
|
|
216
|
-
| 콘텐츠 (8개) | 노드 렌더링 → content/ | hero-title.
|
|
249
|
+
| BG 프레임 (89개) | 프레임 렌더링 → bg/ | hero-bg.webp (4.2MB) |
|
|
250
|
+
| 벡터 글자 (33개) | GROUP 렌더링 → content/ | mission-01.webp (58KB) |
|
|
251
|
+
| 콘텐츠 (8개) | 노드 렌더링 → content/ | hero-title.webp (364KB) |
|
|
217
252
|
| 장식 (29개) | BG 렌더링에 포함 | — |
|
|
218
253
|
→ 전체 159개 → 실제 파일 약 18장
|
|
219
254
|
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
| 이미지 유형 | 렌더링 방법 | 출력 위치 |
|
|
13
13
|
|-----------|-----------|---------|
|
|
14
|
-
| BG 프레임 (합성 배경) | `screenshot {fileKey} {bg.nodeId}` | `bg/{section}-bg.
|
|
15
|
-
| 콘텐츠 (타이틀, 버튼) | `screenshot {fileKey} {node.nodeId}` | `content/{name}.
|
|
16
|
-
| 벡터 글자 그룹 | `screenshot {fileKey} {group.nodeId}` | `content/{name}.
|
|
14
|
+
| BG 프레임 (합성 배경) | `screenshot {fileKey} {bg.nodeId}` | `bg/{section}-bg.webp` |
|
|
15
|
+
| 콘텐츠 (타이틀, 버튼) | `screenshot {fileKey} {node.nodeId}` | `content/{name}.webp` |
|
|
16
|
+
| 벡터 글자 그룹 | `screenshot {fileKey} {group.nodeId}` | `content/{name}.webp` |
|
|
17
17
|
|
|
18
18
|
## BG 프레임 판별
|
|
19
19
|
|
|
@@ -39,18 +39,66 @@ BG 프레임 = 다음 중 하나:
|
|
|
39
39
|
→ 커스텀 폰트 텍스트 = 웹폰트 없음 → 이미지로 사용
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
## 렌더링 금지 노드 (HTML로 구현해야 하는 것)
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
다음 조건에 해당하는 노드는 이미지로 렌더링하지 않는다 — HTML+CSS로 구현:
|
|
46
|
+
|
|
47
|
+
1. TEXT 자식 보유 프레임:
|
|
48
|
+
프레임 내부에 TEXT 노드가 1개 이상 있으면 → HTML로 구현
|
|
49
|
+
⚠️ BG 프레임 렌더링 시에도 TEXT 포함 여부 반드시 확인:
|
|
50
|
+
- BG 프레임에 TEXT 자식이 있으면 → BG 하위만 렌더링 (TEXT 제외)
|
|
51
|
+
- 텍스트 포함된 상위 프레임을 통째 렌더링하면 → 이미지 텍스트 + HTML 텍스트 이중 표시
|
|
52
|
+
|
|
53
|
+
2. INSTANCE 반복 패턴 (카드/아이템 그리드):
|
|
54
|
+
같은 부모 아래 동일 구조 INSTANCE 2개 이상 → HTML 반복 구조 (v-for/.map())
|
|
55
|
+
❌ 카드 그리드를 통째 이미지 1장으로 렌더링 금지
|
|
56
|
+
✅ 각 카드 내부의 이미지 에셋만 개별 추출 (아이콘, 아이템 이미지)
|
|
57
|
+
✅ 카드 레이아웃, 텍스트, 버튼은 HTML로 구현
|
|
58
|
+
|
|
59
|
+
3. 인터랙티브 요소:
|
|
60
|
+
name에 "btn", "button", "CTA", "link", "tab", "toggle" 포함 → HTML <button>/<a>
|
|
61
|
+
❌ 버튼을 이미지로 렌더링 금지 (클릭 이벤트 불가)
|
|
62
|
+
|
|
63
|
+
4. 정보 텍스트 영역:
|
|
64
|
+
기간, 가격, 수량, 설명 등 변경 가능 데이터 → HTML 텍스트
|
|
65
|
+
❌ "1,000", "500 G-COIN", "보상 교환하기" 등을 이미지에 포함 금지
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 이미지로 렌더링하는 것 (HTML로 구현 불가)
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
다음만 이미지로 렌더링:
|
|
72
|
+
|
|
73
|
+
1. 커스텀 폰트 텍스트 (벡터 글자 그룹):
|
|
74
|
+
- 웹폰트 없는 장식 타이틀 ("MISSION 01" 등)
|
|
75
|
+
- VECTOR 타입으로 분해된 글자 → GROUP 렌더링
|
|
76
|
+
|
|
77
|
+
2. 합성 배경 (BG 프레임):
|
|
78
|
+
- 눈, 나무, 파티클 등 장식 레이어 합성물
|
|
79
|
+
- 텍스트 미포함 확인 필수
|
|
80
|
+
|
|
81
|
+
3. 래스터 이미지 에셋:
|
|
82
|
+
- 게임 아이템 썸네일, 코인 아이콘 등
|
|
83
|
+
- imageRef가 있는 개별 RECTANGLE/노드
|
|
84
|
+
|
|
85
|
+
4. 복잡한 벡터 그래픽:
|
|
86
|
+
- CSS로 재현 불가능한 일러스트/아이콘
|
|
87
|
+
- VECTOR/GROUP 조합의 복잡한 그래픽
|
|
88
|
+
```
|
|
89
|
+
|
|
42
90
|
## Format
|
|
43
91
|
|
|
44
|
-
- Output format: `.
|
|
45
|
-
-
|
|
92
|
+
- Output format: `.webp` (Figma API에서 png 수신 → cwebp로 webp 변환)
|
|
93
|
+
- cwebp 미설치 시 `.png` 폴백
|
|
46
94
|
|
|
47
95
|
## Naming
|
|
48
96
|
|
|
49
97
|
렌더링된 이미지의 파일명 = 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.
|
|
98
|
+
- `"Hero"` BG frame → `hero-bg.webp`
|
|
99
|
+
- `"Mission 01"` vector group → `mission-01.webp`
|
|
100
|
+
- `"Title"` content → `hero-title.webp` (섹션명 prefix)
|
|
101
|
+
- `"Btn_Login"` → `btn-login.webp`
|
|
54
102
|
|
|
55
103
|
Rules:
|
|
56
104
|
- 공백 → 하이픈
|