@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.
@@ -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 out = path.join(outDir, ref.slice(0,16) + '.png');
215
+ const outWebp = path.join(outDir, ref.slice(0,16) + '.webp');
182
216
  dl.push(fetch(url).then(r=>r.arrayBuffer()).then(b=>{
183
- fs.writeFileSync(out, Buffer.from(b));
184
- const sz = fs.statSync(out).size;
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
- fs.writeFileSync(outPath, buf);
204
- console.log(JSON.stringify({ path: outPath, size: buf.length, scale }));
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 + '.png';
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) + '.png';
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
- fs.writeFileSync(filePath, Buffer.from(b));
347
- if (fs.statSync(filePath).size > 0) imageMap[ref] = publicPath;
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.png`;
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
- fs.writeFileSync(bgPath, buf);
365
- if (buf.length > 0) {
366
- imageMap[`__bg_${child.nodeId}`] = `images/${bgName}`;
367
- // BG 노드의 imageRef를 합성 이미지로 교체
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.png`);
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
- fs.writeFileSync(screenshotPath, buf);
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: `${sectionPrefix}-screenshot.png`,
448
+ screenshot: path.basename(actualScreenshot),
409
449
  },
410
450
  images: imageMap,
411
451
  imageCount: Object.keys(imageMap).length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@su-record/vibe",
3
- "version": "2.8.46",
3
+ "version": "2.8.48",
4
4
  "description": "AI Coding Framework for Claude Code — 56 agents, 45 skills, multi-LLM orchestration",
5
5
  "type": "module",
6
6
  "main": "dist/cli/index.js",
@@ -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.png
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에서 INSTANCE/COMPONENT 타입 중 아이템 후보를 식별:
370
- - name에 "item", "icon", "reward", "token", "coin", "badge" 포함
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이 아닌 벡터/인스턴스 에셋도 PNG로 확보
376
- → Phase 3에서 데이터의 image 경로에 연결
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}.png
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.png
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.png ← 전체 정답 사진
474
+ ├── full-screenshot.webp ← 전체 정답 사진
421
475
  ├── tree.json ← 노드 트리 + CSS 수치
422
476
  ├── images/ ← 모든 이미지 에셋
423
- │ ├── hero-bg.png
424
- │ ├── hero-title.png
425
- │ ├── card-item-1.png
477
+ │ ├── hero-bg.webp
478
+ │ ├── hero-title.webp
479
+ │ ├── card-item-1.webp
426
480
  │ └── ...
427
481
  └── sections/ ← 섹션별 정답 사진
428
- ├── hero.png
429
- ├── daily-checkin.png
430
- ├── playtime-mission.png
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.png → frame-2/full-screenshot.png → ...
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
- a. 이미지: 노드 렌더링된 이미지를 static/images/{feature}/에 배치
665
- b. 노드 HTML 매핑:
666
- - BG 프레임 → CSS background-image (img 태그 아님)
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
- - 콘텐츠 이미지 → <img src="렌더링된 파일">
671
- - 반복 패턴 (동일 구조 3+) → v-for
672
- c. CSS 직접 매핑:
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}/파일명.png (실제 파일 존재 확인)
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}.png',
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}.png', // Figma 원본
880
- '/tmp/{feature}/rendered-{section}.png', // 렌더링 결과
881
- '/tmp/{feature}/diff-{section}.png', // 차이 시각화
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}}.png` | {{bg\|content\|vector-text}} | {{frame-render\|node-render\|group-render}} |
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.png" },
47
- "pc": { "nodeId": "...", "file": "pc-main/bg/hero-bg.png" }
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.png", "size": { "w": 620, "h": 174 } },
67
- "pc": { "nodeId": "...", "file": "pc-main/content/hero-title.png", "size": { "w": 1200, "h": 340 } }
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.png", "size": { "w": 586, "h": 32 } },
76
- "pc": { "nodeId": "...", "file": "pc-main/content/hero-subtitle.png", "size": { "w": 1100, "h": 60 } }
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.png" },
97
- "pc": { "nodeId": "...", "file": "pc-main/bg/period-bg.png" }
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 수 동일) 노드 3개 이상
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.png)
335
- │ └── Sub Title (VECTOR — imageRef → subtitle.png)
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.png'); background-size: cover; } -->
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.png"
395
+ <img src="/images/{feature}/title.webp"
354
396
  alt="추운 겨울, 따뜻한 보상이 펑펑"
355
397
  class="heroTitleImg" />
356
- <img src="/images/{feature}/subtitle.png"
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.png" alt="공유하기" class="heroShareIcon" />
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.png); }
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
- 부모와 동일 크기(±5%) + imageRef + 형제 중 첫 위치:
25
- `position: absolute; inset: 0; z-index: 0; object-fit: cover`
26
- 부모에 `position: relative; overflow: hidden` 추가
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}}.png'); background-size: cover; } -->
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.png');
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.png
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
- 대상 식별 (tree.json에서):
126
- - 타이틀/서브타이틀 이미지 (name에 "title", "sub title")
127
- - 버튼 이미지 (name에 "btn", "button")
171
+ ⚠️ 렌더링 전 2-1.5 체크 필수 — 하나라도 해당하면 이미지 렌더링 금지
172
+
173
+ 대상 식별 (tree.json에서 TEXT 자식 미보유 노드만):
174
+ - 타이틀/서브타이틀 이미지 (name에 "title", "sub title") — 벡터 글자만, TEXT 노드 아님
128
175
  - 아이콘 (VECTOR/GROUP 크기 ≤ 64px)
129
- - 아이템/보상 이미지 (name에 "item", "reward", "token", "coin")
176
+ - 아이템/보상 썸네일 (name에 "item", "reward", "token", "coin") — 이미지 에셋만
130
177
 
131
178
  렌더링 (imageRef 다운로드 아님!):
132
- node "[FIGMA_SCRIPT]" screenshot {fileKey} {node.nodeId} --out=/tmp/{feature}/content/{name}.png
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}.png
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.png
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}.png
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.png
198
- │ ├── daily-bg.png
244
+ │ ├── hero-bg.webp
245
+ │ ├── daily-bg.webp
199
246
  │ └── ...
200
247
  ├── content/ ← 콘텐츠 노드 렌더링
201
- │ ├── hero-title.png
202
- │ ├── hero-subtitle.png
203
- │ ├── mission-01.png ← 벡터 글자 그룹 렌더링
204
- │ ├── btn-login.png
248
+ │ ├── hero-title.webp
249
+ │ ├── hero-subtitle.webp
250
+ │ ├── mission-01.webp ← 벡터 글자 그룹 렌더링
251
+ │ ├── btn-login.webp
205
252
  │ └── ...
206
- ├── full-screenshot.png ← Phase 4 검증용
253
+ ├── full-screenshot.webp ← Phase 4 검증용
207
254
  └── sections/ ← Phase 4 섹션별 검증용
208
- ├── hero.png
255
+ ├── hero.webp
209
256
  └── ...
210
257
 
211
258
  이미지 분류 (실제 테스트 기준):
212
259
  | 분류 | 처리 | 예시 |
213
260
  |------|------|------|
214
- | BG 프레임 (89개) | 프레임 렌더링 → bg/ | hero-bg.png (4.2MB) |
215
- | 벡터 글자 (33개) | GROUP 렌더링 → content/ | mission-01.png (58KB) |
216
- | 콘텐츠 (8개) | 노드 렌더링 → content/ | hero-title.png (364KB) |
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.png` |
15
- | 콘텐츠 (타이틀, 버튼) | `screenshot {fileKey} {node.nodeId}` | `content/{name}.png` |
16
- | 벡터 글자 그룹 | `screenshot {fileKey} {group.nodeId}` | `content/{name}.png` |
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: `.png` (Figma screenshot API 기본)
45
- - 최적화 필요빌드 단계에서 webp 변환 (추출 단계에서는 png)
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.png`
51
- - `"Mission 01"` vector group → `mission-01.png`
52
- - `"Title"` content → `hero-title.png` (섹션명 prefix)
53
- - `"Btn_Login"` → `btn-login.png`
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
  - 공백 → 하이픈