@su-record/vibe 2.8.45 → 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 +41 -16
- package/skills/vibe.figma.convert/rubrics/conversion-rules.md +33 -3
- package/skills/vibe.figma.convert/templates/component.md +3 -5
- package/skills/vibe.figma.extract/SKILL.md +60 -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 },
|
|
@@ -68,9 +68,14 @@ component-index (/tmp/{feature}/component-index.json) 에서 매칭되는 컴포
|
|
|
68
68
|
children 없음 → 빈 div 또는 스킵
|
|
69
69
|
|
|
70
70
|
2. 배경 레이어 판별:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
BG 프레임 (name에 "BG"/"bg" 또는 부모와 동일 크기)
|
|
72
|
+
❌ <img> 태그로 배경 처리 금지
|
|
73
|
+
❌ position:absolute + inset:0 으로 이미지 배치 금지
|
|
74
|
+
✅ 부모 요소에 CSS background-image로 처리:
|
|
75
|
+
background-image: url('/images/{feature}/{section}-bg.webp');
|
|
76
|
+
background-size: cover;
|
|
77
|
+
background-position: center top;
|
|
78
|
+
✅ BG 프레임은 HTML에 아무것도 렌더링하지 않음 (CSS만)
|
|
74
79
|
|
|
75
80
|
3. 반복 패턴 감지:
|
|
76
81
|
같은 부모 아래 동일 타입 + 유사 구조(children 수 동일) 노드 3개 이상
|
|
@@ -192,6 +197,27 @@ tree.json의 css 객체를 SCSS로 직접 변환한다. 추정하지 않는다.
|
|
|
192
197
|
|
|
193
198
|
## 2. 외부 SCSS 파일 구조
|
|
194
199
|
|
|
200
|
+
### _base.scss (필수 — 래퍼 컨테이너)
|
|
201
|
+
|
|
202
|
+
```scss
|
|
203
|
+
// 모바일 퍼스트: vw 단위가 PC에서 거대해지는 것을 방지
|
|
204
|
+
// designWidth(720px)를 max-width로 제한
|
|
205
|
+
|
|
206
|
+
.{feature} {
|
|
207
|
+
width: 100%;
|
|
208
|
+
max-width: 720px; // designWidth — PC에서 모바일 레이아웃 유지
|
|
209
|
+
margin: 0 auto; // 중앙 정렬
|
|
210
|
+
overflow-x: hidden;
|
|
211
|
+
|
|
212
|
+
// PC 브레이크포인트에서 max-width 확장 (PC 디자인이 있을 때)
|
|
213
|
+
@media (min-width: 1025px) {
|
|
214
|
+
max-width: 100%; // PC 디자인이 있으면 전체 너비
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
이 파일이 없으면 vw 단위가 PC 뷰포트에서 비례 확대되어 레이아웃이 깨진다.
|
|
220
|
+
|
|
195
221
|
### layout vs components 구분
|
|
196
222
|
|
|
197
223
|
```
|
|
@@ -305,8 +331,8 @@ $bp-desktop: 1024px;
|
|
|
305
331
|
Hero (INSTANCE 720x1280)
|
|
306
332
|
├── BG (FRAME — 배경 레이어)
|
|
307
333
|
├── Title (FRAME — flex-column, gap:24px)
|
|
308
|
-
│ ├── Title (RECTANGLE — imageRef → title.
|
|
309
|
-
│ └── Sub Title (VECTOR — imageRef → subtitle.
|
|
334
|
+
│ ├── Title (RECTANGLE — imageRef → title.webp)
|
|
335
|
+
│ └── Sub Title (VECTOR — imageRef → subtitle.webp)
|
|
310
336
|
├── Period (FRAME — flex-column, gap:10px, padding)
|
|
311
337
|
│ └── Period (FRAME — flex-column, gap:22px)
|
|
312
338
|
│ ├── Period_Left (FRAME — flex-column, gap:4px)
|
|
@@ -319,17 +345,15 @@ $bp-desktop: 1024px;
|
|
|
319
345
|
-->
|
|
320
346
|
<template>
|
|
321
347
|
<section class="heroSection">
|
|
322
|
-
<!-- BG:
|
|
323
|
-
|
|
324
|
-
<img src="/images/{feature}/hero-bg.png" alt="" aria-hidden="true" />
|
|
325
|
-
</div>
|
|
348
|
+
<!-- BG: CSS background-image로 처리 (img 태그 아님!) -->
|
|
349
|
+
<!-- .heroSection { background-image: url('/images/{feature}/hero-bg.webp'); background-size: cover; } -->
|
|
326
350
|
|
|
327
351
|
<!-- Title: flex-column, gap:24px → 직접 매핑 -->
|
|
328
352
|
<div class="heroTitle">
|
|
329
|
-
<img src="/images/{feature}/title.
|
|
353
|
+
<img src="/images/{feature}/title.webp"
|
|
330
354
|
alt="추운 겨울, 따뜻한 보상이 펑펑"
|
|
331
355
|
class="heroTitleImg" />
|
|
332
|
-
<img src="/images/{feature}/subtitle.
|
|
356
|
+
<img src="/images/{feature}/subtitle.webp"
|
|
333
357
|
alt="겨울을 녹일 보상, 지금 PC방에서 획득하세요!"
|
|
334
358
|
class="heroSubtitleImg" />
|
|
335
359
|
</div>
|
|
@@ -353,7 +377,7 @@ $bp-desktop: 1024px;
|
|
|
353
377
|
|
|
354
378
|
<!-- BTN_Share: flex, borderRadius:500px → 버튼으로 승격 -->
|
|
355
379
|
<button class="heroShareBtn" @click="handleShare">
|
|
356
|
-
<img src="/images/{feature}/share-icon.
|
|
380
|
+
<img src="/images/{feature}/share-icon.webp" alt="공유하기" class="heroShareIcon" />
|
|
357
381
|
</button>
|
|
358
382
|
</section>
|
|
359
383
|
</template>
|
|
@@ -407,9 +431,10 @@ function handleShare(): void {
|
|
|
407
431
|
트리 노드의 속성으로 이미지 유형을 판별한다:
|
|
408
432
|
|
|
409
433
|
배경 이미지:
|
|
410
|
-
조건:
|
|
411
|
-
|
|
412
|
-
|
|
434
|
+
조건: BG 프레임 (name에 BG/bg 또는 부모와 크기 동일)
|
|
435
|
+
❌ <img> 태그 금지
|
|
436
|
+
✅ CSS background-image로만 처리:
|
|
437
|
+
부모 { background-image: url('...'); background-size: cover; }
|
|
413
438
|
|
|
414
439
|
콘텐츠 이미지:
|
|
415
440
|
조건: imageRef 있음 + 독립적 크기 + TEXT 형제 없음
|
|
@@ -441,7 +466,7 @@ function handleShare(): void {
|
|
|
441
466
|
같은 값 → 유지
|
|
442
467
|
다른 px 값 → @include pc { width: {desktop값 × pcScaleFactor}px; }
|
|
443
468
|
다른 레이아웃 → @include pc { flex-direction: row; }
|
|
444
|
-
다른 이미지 → @include pc { content: url(/images/{feature}/desktop-xxx.
|
|
469
|
+
다른 이미지 → @include pc { content: url(/images/{feature}/desktop-xxx.webp); }
|
|
445
470
|
|
|
446
471
|
기존 모바일 코드 삭제 금지.
|
|
447
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
|
|
|
@@ -20,10 +20,8 @@ tree.json 노드를 기반으로 컴포넌트를 생성할 때 이 템플릿을
|
|
|
20
20
|
-->
|
|
21
21
|
<template>
|
|
22
22
|
<section class="{{sectionName}}">
|
|
23
|
-
<!-- BG:
|
|
24
|
-
|
|
25
|
-
<img src="/images/{{FEATURE_KEY}}/{{bg-file}}.png" alt="" aria-hidden="true" />
|
|
26
|
-
</div>
|
|
23
|
+
<!-- BG: CSS background-image로 처리. img 태그 사용 금지! -->
|
|
24
|
+
<!-- SCSS: .{{sectionName}} { background-image: url('/images/{{FEATURE_KEY}}/{{bg-file}}.webp'); background-size: cover; } -->
|
|
27
25
|
|
|
28
26
|
<!-- {{CHILD_1}}: tree flex-column, gap:{{GAP}}px → 직접 매핑 -->
|
|
29
27
|
<div class="{{sectionName}}__{{child1Name}}">
|
|
@@ -88,7 +86,7 @@ defineProps<{
|
|
|
88
86
|
width: 100%;
|
|
89
87
|
height: {{HEIGHT_VW}}vw; // tree: {{HEIGHT}} / {{DESIGN_WIDTH}} × 100
|
|
90
88
|
overflow: hidden; // tree: overflow:hidden
|
|
91
|
-
background-image: url('/images/{{FEATURE_KEY}}/{{section}}-bg.
|
|
89
|
+
background-image: url('/images/{{FEATURE_KEY}}/{{section}}-bg.webp');
|
|
92
90
|
background-size: cover;
|
|
93
91
|
background-position: center top;
|
|
94
92
|
}
|
|
@@ -99,32 +99,72 @@ Bash:
|
|
|
99
99
|
```
|
|
100
100
|
각 섹션의 BG 프레임을 식별 → 합성된 배경 1장으로 렌더링:
|
|
101
101
|
|
|
102
|
+
⚠️ 주의: BG 프레임만 렌더링한다 (텍스트 포함된 상위 프레임 렌더링 금지)
|
|
103
|
+
❌ 섹션 전체를 렌더링 → 텍스트가 이미지에 포함 → HTML 텍스트와 중복
|
|
104
|
+
❌ TEXT 자식이 있는 프레임을 렌더링 → 이미지 텍스트 + HTML 텍스트 이중 표시
|
|
105
|
+
✅ BG 하위 프레임만 렌더링 → 텍스트 없는 배경만 → CSS background-image
|
|
106
|
+
✅ 텍스트는 tree.json에서 추출하여 HTML로 작성
|
|
107
|
+
|
|
108
|
+
렌더링 전 TEXT 자식 검증 (BLOCKING):
|
|
109
|
+
BG 프레임의 전체 자식 트리를 순회하여 TEXT 노드 존재 여부 확인
|
|
110
|
+
TEXT 노드 발견 시:
|
|
111
|
+
→ TEXT 노드가 포함된 하위 프레임은 렌더링에서 제외
|
|
112
|
+
→ BG 프레임 내 순수 시각 요소(이미지, 벡터, 장식)만 렌더링
|
|
113
|
+
→ 또는 TEXT가 없는 가장 깊은 BG 하위 프레임을 개별 렌더링
|
|
114
|
+
|
|
102
115
|
BG 프레임 판별 기준:
|
|
103
116
|
- name에 "BG", "bg" 포함
|
|
104
117
|
- 또는 부모와 크기 동일(±10%) + 자식 이미지 3개 이상
|
|
105
118
|
- 또는 1depth 첫 번째 자식이면서 이미지 노드 다수 보유
|
|
106
119
|
|
|
107
120
|
렌더링:
|
|
108
|
-
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
|
|
109
122
|
|
|
110
123
|
→ BG 하위 20+ 레이어가 합성된 1장
|
|
111
124
|
→ CSS background-image로 처리
|
|
112
125
|
→ 개별 레이어(눈, 나무, 파티클 등) 다운로드하지 않음
|
|
113
126
|
```
|
|
114
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
|
+
|
|
115
154
|
### 2-2. 콘텐츠 노드 렌더링
|
|
116
155
|
|
|
117
156
|
```
|
|
118
157
|
BG가 아닌 콘텐츠 이미지를 개별 노드 렌더링:
|
|
119
158
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
159
|
+
⚠️ 렌더링 전 2-1.5 체크 필수 — 하나라도 해당하면 이미지 렌더링 금지
|
|
160
|
+
|
|
161
|
+
대상 식별 (tree.json에서 — TEXT 자식 미보유 노드만):
|
|
162
|
+
- 타이틀/서브타이틀 이미지 (name에 "title", "sub title") — 벡터 글자만, TEXT 노드 아님
|
|
123
163
|
- 아이콘 (VECTOR/GROUP 크기 ≤ 64px)
|
|
124
|
-
- 아이템/보상
|
|
164
|
+
- 아이템/보상 썸네일 (name에 "item", "reward", "token", "coin") — 이미지 에셋만
|
|
125
165
|
|
|
126
166
|
렌더링 (imageRef 다운로드 아님!):
|
|
127
|
-
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
|
|
128
168
|
|
|
129
169
|
→ 텍스처 fill이 적용된 최종 결과물이 나옴
|
|
130
170
|
→ 22.7MB 텍스처 대신 364KB 렌더링 이미지
|
|
@@ -142,7 +182,7 @@ BG가 아닌 콘텐츠 이미지를 개별 노드 렌더링:
|
|
|
142
182
|
|
|
143
183
|
렌더링:
|
|
144
184
|
부모 GROUP을 통째로 렌더링 (개별 글자 다운로드 금지)
|
|
145
|
-
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
|
|
146
186
|
|
|
147
187
|
예시:
|
|
148
188
|
"MISSION 01" GROUP (174x42, 벡터 9개) → 렌더링 1장 (58KB)
|
|
@@ -168,10 +208,10 @@ BG가 아닌 콘텐츠 이미지를 개별 노드 렌더링:
|
|
|
168
208
|
코드 생성에는 사용하지 않는다. Phase 4 시각 검증에서만 사용.
|
|
169
209
|
|
|
170
210
|
전체 스크린샷:
|
|
171
|
-
node "[FIGMA_SCRIPT]" screenshot {fileKey} {nodeId} --out=/tmp/{feature}/full-screenshot.
|
|
211
|
+
node "[FIGMA_SCRIPT]" screenshot {fileKey} {nodeId} --out=/tmp/{feature}/full-screenshot.webp
|
|
172
212
|
|
|
173
213
|
섹션별 스크린샷 (1depth 자식 프레임 각각):
|
|
174
|
-
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
|
|
175
215
|
|
|
176
216
|
용도:
|
|
177
217
|
✅ Phase 4에서 렌더링 결과와 pixelmatch 비교
|
|
@@ -189,26 +229,26 @@ BG가 아닌 콘텐츠 이미지를 개별 노드 렌더링:
|
|
|
189
229
|
/tmp/{feature}/
|
|
190
230
|
├── tree.json ← 코드 생성의 PRIMARY 소스
|
|
191
231
|
├── bg/ ← BG 프레임 렌더링 (섹션당 1장)
|
|
192
|
-
│ ├── hero-bg.
|
|
193
|
-
│ ├── daily-bg.
|
|
232
|
+
│ ├── hero-bg.webp
|
|
233
|
+
│ ├── daily-bg.webp
|
|
194
234
|
│ └── ...
|
|
195
235
|
├── content/ ← 콘텐츠 노드 렌더링
|
|
196
|
-
│ ├── hero-title.
|
|
197
|
-
│ ├── hero-subtitle.
|
|
198
|
-
│ ├── mission-01.
|
|
199
|
-
│ ├── btn-login.
|
|
236
|
+
│ ├── hero-title.webp
|
|
237
|
+
│ ├── hero-subtitle.webp
|
|
238
|
+
│ ├── mission-01.webp ← 벡터 글자 그룹 렌더링
|
|
239
|
+
│ ├── btn-login.webp
|
|
200
240
|
│ └── ...
|
|
201
|
-
├── full-screenshot.
|
|
241
|
+
├── full-screenshot.webp ← Phase 4 검증용
|
|
202
242
|
└── sections/ ← Phase 4 섹션별 검증용
|
|
203
|
-
├── hero.
|
|
243
|
+
├── hero.webp
|
|
204
244
|
└── ...
|
|
205
245
|
|
|
206
246
|
이미지 분류 (실제 테스트 기준):
|
|
207
247
|
| 분류 | 처리 | 예시 |
|
|
208
248
|
|------|------|------|
|
|
209
|
-
| BG 프레임 (89개) | 프레임 렌더링 → bg/ | hero-bg.
|
|
210
|
-
| 벡터 글자 (33개) | GROUP 렌더링 → content/ | mission-01.
|
|
211
|
-
| 콘텐츠 (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) |
|
|
212
252
|
| 장식 (29개) | BG 렌더링에 포함 | — |
|
|
213
253
|
→ 전체 159개 → 실제 파일 약 18장
|
|
214
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
|
- 공백 → 하이픈
|