@su-record/vibe 2.8.33 → 2.8.35

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.
@@ -191,14 +191,225 @@ async function cmdImages(token, fk, nid, outDir, depth) {
191
191
 
192
192
  async function cmdScreenshot(token, fk, nid, outPath) {
193
193
  if (!outPath) fail('--out required');
194
- const data = await apiFetch(`/images/${fk}?ids=${nid}&format=png&scale=2`, token);
195
- const url = data.images?.[nid];
196
- if (!url) fail(`No image for ${nid}`);
197
194
  const dir = path.dirname(outPath);
198
195
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
199
- const buf = Buffer.from(await (await fetch(url)).arrayBuffer());
200
- fs.writeFileSync(outPath, buf);
201
- console.log(JSON.stringify({ path: outPath, size: buf.length }));
196
+ // scale=2 시도 400 에러 시 scale=1 폴백
197
+ for (const scale of [2, 1]) {
198
+ try {
199
+ const data = await apiFetch(`/images/${fk}?ids=${nid}&format=png&scale=${scale}`, token);
200
+ const url = data.images?.[nid];
201
+ if (!url) continue;
202
+ 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 }));
205
+ return;
206
+ } catch (e) {
207
+ if (scale === 1) fail(`Screenshot failed: ${e.message}`);
208
+ }
209
+ }
210
+ fail('Screenshot failed at all scales');
211
+ }
212
+
213
+ // ─── Render: HTML + SCSS + Images + Screenshot ─────────────────────
214
+
215
+ function kebab(str) {
216
+ return str
217
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
218
+ .replace(/[\s_/\\:]+/g, '-')
219
+ .replace(/[^a-zA-Z0-9-]/g, '') // 한글 제거 — 클래스명은 영문만
220
+ .replace(/-+/g, '-')
221
+ .replace(/^-|-$/g, '')
222
+ .toLowerCase()
223
+ .slice(0, 30); // 최대 30자
224
+ }
225
+
226
+ function nodeName(node, parentPrefix) {
227
+ let raw = node.name || '';
228
+ // TEXT 노드: 텍스트 대신 부모 컨텍스트 + 'text' 사용
229
+ if (node.type === 'TEXT') raw = raw.slice(0, 20) || 'text';
230
+ // 의미 없는 이름 정리
231
+ raw = raw.replace(/^(Frame|Group|Rectangle|Vector|Ellipse)\s*/i, '');
232
+ if (!raw || /^\d+$/.test(raw)) raw = node.type?.toLowerCase() || 'node';
233
+ const k = kebab(raw);
234
+ return parentPrefix ? `${parentPrefix}-${k}` : k;
235
+ }
236
+
237
+ /** 노드 트리 → HTML 문자열 생성 */
238
+ function toHTML(node, prefix, imgMap, indent = 0) {
239
+ const pad = ' '.repeat(indent);
240
+ const cls = nodeName(node, prefix);
241
+ const lines = [];
242
+
243
+ // 이미지 노드
244
+ if (node.imageRef && imgMap[node.imageRef]) {
245
+ const isDecorative = node.name?.match(/^(BG|bg|배경|Shadow|Glow|Light|snow|눈|얼음|빙판|트리|Particle)/i);
246
+ const alt = isDecorative ? '' : (node.name || '');
247
+ const ariaHidden = isDecorative ? ' aria-hidden="true"' : '';
248
+ lines.push(`${pad}<img class="${cls}" src="${imgMap[node.imageRef]}" alt="${alt}"${ariaHidden} />`);
249
+ return lines.join('\n');
250
+ }
251
+
252
+ // 텍스트 노드
253
+ if (node.type === 'TEXT' && node.text) {
254
+ const tag = node.text.length > 100 ? 'p' : 'span';
255
+ lines.push(`${pad}<${tag} class="${cls}">${node.text}</${tag}>`);
256
+ return lines.join('\n');
257
+ }
258
+
259
+ // 컨테이너 노드
260
+ const tag = node.type === 'TEXT' ? 'p' : 'div';
261
+ if (!node.children?.length) {
262
+ lines.push(`${pad}<${tag} class="${cls}" />`);
263
+ return lines.join('\n');
264
+ }
265
+
266
+ lines.push(`${pad}<${tag} class="${cls}">`);
267
+ for (const child of node.children) {
268
+ lines.push(toHTML(child, cls, imgMap, indent + 1));
269
+ }
270
+ lines.push(`${pad}</${tag}>`);
271
+ return lines.join('\n');
272
+ }
273
+
274
+ /** 노드 트리 → SCSS 문자열 생성 */
275
+ function toSCSS(node, prefix, indent = 0) {
276
+ const cls = nodeName(node, prefix);
277
+ const lines = [];
278
+ const css = node.css || {};
279
+ const props = Object.entries(css);
280
+
281
+ if (props.length) {
282
+ lines.push(`.${cls} {`);
283
+ for (const [k, v] of props) {
284
+ // camelCase → kebab-case
285
+ const prop = k.replace(/([A-Z])/g, '-$1').toLowerCase();
286
+ lines.push(` ${prop}: ${v};`);
287
+ }
288
+ lines.push('}');
289
+ lines.push('');
290
+ }
291
+
292
+ if (node.children?.length) {
293
+ for (const child of node.children) {
294
+ lines.push(toSCSS(child, cls));
295
+ }
296
+ }
297
+ return lines.join('\n');
298
+ }
299
+
300
+ /** imageRef → 이름 기반 매핑 (해시 아닌 노드 name 사용) */
301
+ function buildImageNames(node, prefix, result = {}) {
302
+ if (node.imageRef) {
303
+ const name = nodeName(node, prefix);
304
+ result[node.imageRef] = name + '.png';
305
+ }
306
+ if (node.children) {
307
+ for (const child of node.children) {
308
+ buildImageNames(child, nodeName(node, prefix) || prefix, result);
309
+ }
310
+ }
311
+ return result;
312
+ }
313
+
314
+ async function cmdRender(token, fk, nid, outDir, depth, scale) {
315
+ if (!outDir) fail('--out=<dir> required');
316
+ if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
317
+
318
+ const imgDir = path.join(outDir, 'images');
319
+ if (!fs.existsSync(imgDir)) fs.mkdirSync(imgDir, { recursive: true });
320
+
321
+ // 1. 트리 가져오기
322
+ const dp = depth ? `&depth=${depth}` : '';
323
+ const treeData = await apiFetch(`/files/${fk}/nodes?ids=${nid}${dp}`, token);
324
+ const nd = treeData.nodes?.[nid];
325
+ if (!nd?.document) fail(`Node ${nid} not found`);
326
+ const tree = walk(nd.document);
327
+
328
+ // 2. 이미지 이름 매핑 (노드 name 기반)
329
+ const sectionPrefix = kebab(tree.name);
330
+ const imageNames = buildImageNames(tree, sectionPrefix);
331
+ const refs = collectRefs(tree);
332
+
333
+ // 3. fill 이미지 다운로드 (이름 기반)
334
+ let imageMap = {};
335
+ if (refs.size) {
336
+ const allImg = await apiFetch(`/files/${fk}/images`, token);
337
+ const urls = allImg.meta?.images || {};
338
+ const dl = [];
339
+ for (const ref of refs) {
340
+ const url = urls[ref];
341
+ if (!url) continue;
342
+ const fileName = imageNames[ref] || ref.slice(0, 16) + '.png';
343
+ const filePath = path.join(imgDir, fileName);
344
+ const publicPath = `images/${fileName}`;
345
+ 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;
348
+ }).catch(() => {}));
349
+ }
350
+ await Promise.all(dl);
351
+ }
352
+
353
+ // 4. 복합 BG 노드 → 스크린샷으로 렌더링
354
+ for (const child of tree.children || []) {
355
+ if (/^(BG|bg|배경)$/i.test(child.name) && child.children?.length > 3) {
356
+ const bgName = `${sectionPrefix}-bg-composite.png`;
357
+ const bgPath = path.join(imgDir, bgName);
358
+ try {
359
+ for (const s of [2, 1]) {
360
+ const sData = await apiFetch(`/images/${fk}?ids=${child.nodeId}&format=png&scale=${s}`, token);
361
+ const sUrl = sData.images?.[child.nodeId];
362
+ if (!sUrl) continue;
363
+ 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를 합성 이미지로 교체
368
+ child.imageRef = `__bg_${child.nodeId}`;
369
+ child.children = []; // 하위 제거 (합성됨)
370
+ break;
371
+ }
372
+ }
373
+ } catch { /* BG screenshot failed, keep original structure */ }
374
+ }
375
+ }
376
+
377
+ // 5. 스크린샷
378
+ const screenshotPath = path.join(outDir, `${sectionPrefix}-screenshot.png`);
379
+ try {
380
+ for (const s of [2, 1]) {
381
+ const sData = await apiFetch(`/images/${fk}?ids=${nid}&format=png&scale=${s}`, token);
382
+ const sUrl = sData.images?.[nid];
383
+ if (!sUrl) continue;
384
+ const buf = Buffer.from(await (await fetch(sUrl)).arrayBuffer());
385
+ fs.writeFileSync(screenshotPath, buf);
386
+ break;
387
+ }
388
+ } catch { /* screenshot failed */ }
389
+
390
+ // 6. HTML 생성
391
+ const html = toHTML(tree, '', imageMap);
392
+ fs.writeFileSync(path.join(outDir, `${sectionPrefix}.html`), html);
393
+
394
+ // 7. SCSS 생성
395
+ const scss = toSCSS(tree, '');
396
+ fs.writeFileSync(path.join(outDir, `${sectionPrefix}.scss`), scss);
397
+
398
+ // 8. 트리 JSON 저장
399
+ fs.writeFileSync(path.join(outDir, `${sectionPrefix}.json`), JSON.stringify(tree, null, 2));
400
+
401
+ // 출력 요약
402
+ console.log(JSON.stringify({
403
+ section: sectionPrefix,
404
+ files: {
405
+ html: `${sectionPrefix}.html`,
406
+ scss: `${sectionPrefix}.scss`,
407
+ json: `${sectionPrefix}.json`,
408
+ screenshot: `${sectionPrefix}-screenshot.png`,
409
+ },
410
+ images: imageMap,
411
+ imageCount: Object.keys(imageMap).length,
412
+ }, null, 2));
202
413
  }
203
414
 
204
415
  // ─── CLI ────────────────────────────────────────────────────────────
@@ -221,5 +432,6 @@ switch (cmd) {
221
432
  case 'tree': await cmdTree(token, fk, nid, flags.depth ? +flags.depth : undefined); break;
222
433
  case 'images': await cmdImages(token, fk, nid, flags.out, flags.depth ? +flags.depth : 10); break;
223
434
  case 'screenshot': await cmdScreenshot(token, fk, nid, flags.out); break;
224
- default: console.log('Usage: node figma-extract.js <tree|images|screenshot> <fileKey> <nodeId> [flags]');
435
+ case 'render': await cmdRender(token, fk, nid, flags.out, flags.depth ? +flags.depth : 10, flags.scale ? +flags.scale : 0.667); break;
436
+ default: console.log('Usage: node figma-extract.js <tree|images|screenshot|render> <fileKey> <nodeId> [flags]');
225
437
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@su-record/vibe",
3
- "version": "2.8.33",
3
+ "version": "2.8.35",
4
4
  "description": "AI Coding Framework for Claude Code — 49 agents, 41+ tools, multi-LLM orchestration",
5
5
  "type": "module",
6
6
  "main": "dist/cli/index.js",
@@ -68,16 +68,15 @@ tier: standard
68
68
 
69
69
  ```
70
70
  URL에서 fileKey, nodeId 추출
71
- getTree(fileKey, nodeId, depth=2) → 프레임 목록
72
71
 
73
- Bash:
74
- node -e "
75
- import { getTree } from './dist/infra/lib/figma/index.js';
76
- const tree = await getTree({ fileKey: '{fileKey}', nodeId: '{nodeId}', depth: 2 });
77
- console.log(JSON.stringify(tree));
78
- "
72
+ 1단계 (BLOCKING): 루트 depth=2로 전체 프레임 + nodeId 수집
73
+ # [FIGMA_SCRIPT] = ~/.vibe/hooks/scripts/figma-extract.js
74
+ node "[FIGMA_SCRIPT]" tree {fileKey} {nodeId} --depth=2
75
+
76
+ → 모든 자식 프레임의 name + nodeId + size 를 테이블로 출력
77
+ → nodeId가 빠진 프레임이 있으면 안 됨
79
78
 
80
- 프레임 분류 (name 패턴 기반):
79
+ 2단계: name 패턴으로 프레임 분류
81
80
  SPEC — "기능 정의서", "정책" → depth 높여서 텍스트 추출
82
81
  CONFIG — "해상도", "브라우저" → 스케일 팩터 계산
83
82
  SHARED — "공통", "GNB", "Footer", "Popup" → 공통 컴포넌트 파악
@@ -91,7 +90,7 @@ Bash:
91
90
  4순위: SHARED (공통 요소, Popup) — 필요 시
92
91
 
93
92
  높이 1500px 이상 프레임:
94
- getScreenshot으로 시각 파악
93
+ node "[FIGMA_SCRIPT]" screenshot으로 시각 파악
95
94
  → 또는 depth 높여서 하위 분할 조회
96
95
  ```
97
96
 
@@ -226,129 +225,58 @@ SCSS 파일 기본 내용 Write:
226
225
 
227
226
  ### 2-2. 섹션 루프
228
227
 
229
- **각 섹션을 순서대로, 섹션을 완전히 완료한 다음으로.**
228
+ **섹션별로 a→d 순서는 지키되, 섹션 병렬 처리 허용.**
229
+ **단, 첫 번째 섹션(Hero)은 단독 완료 후 나머지를 병렬로.**
230
230
 
231
- #### a. 노드 트리 + CSS 추출
231
+ #### a. render 실행 (BLOCKING)
232
232
 
233
233
  ```
234
- Figma REST API로 노드 트리와 CSS 속성을 직접 추출한다.
235
- MCP 플러그인(get_design_context/get_metadata)은 사용하지 않는다.
236
-
237
- Bash:
238
- # [FIGMA_SCRIPT] = ~/.vibe/hooks/scripts/figma-extract.js
239
- node "[FIGMA_SCRIPT]" tree {fileKey} {섹션.nodeId} --depth=10
240
-
241
- 반환 (JSON):
242
- {
243
- nodeId, name, type, size: {width, height},
244
- css: { display, flexDirection, gap, fontSize, color, ... },
245
- text: "텍스트 내용" (TEXT 노드),
246
- imageRef: "abc123" (이미지 fill),
247
- children: [...]
248
- }
249
-
250
- CSS는 Figma 노드 속성에서 직접 추출 — Tailwind 역변환 불필요:
251
- fills → background-color effects → box-shadow, filter
252
- strokes → border style → font-family, font-size, color
253
- layoutMode → display:flex itemSpacing → gap
254
- padding* → padding cornerRadius → border-radius
255
-
256
- 인스턴스 내부 자식도 depth로 전부 조회 가능 (MCP 한계 해결).
257
- ```
234
+ # [FIGMA_SCRIPT] = ~/.vibe/hooks/scripts/figma-extract.js
235
+ node "[FIGMA_SCRIPT]" render {fileKey} {섹션.nodeId} --out=/tmp/{feature}-{section}/ --depth=10
258
236
 
259
- #### b. 이미지 다운로드 (BLOCKING)
237
+ 호출로 아래 파일이 생성됨:
238
+ /tmp/{feature}-{section}/
239
+ ├── {section}.html ← HTML 구조 (class명 포함)
240
+ ├── {section}.scss ← 전체 SCSS (모든 CSS 속성)
241
+ ├── {section}.json ← 원본 트리 JSON
242
+ ├── {section}-screenshot.png ← 섹션 스크린샷 (시각 기준점)
243
+ └── images/
244
+ ├── {section}-bg-composite.png ← 복합 BG 합성 이미지
245
+ ├── {section}-title.png ← 이름 있는 이미지
246
+ └── ...
260
247
 
248
+ 이미지는 노드 name 기반 파일명 (해시 아님).
249
+ 복합 BG(3개+ 하위 레이어)는 자동으로 합성 스크린샷 생성.
250
+ 인스턴스 내부 자식도 depth로 전부 조회.
261
251
  ```
262
- 트리에서 imageRef가 있는 노드를 수집 → Figma API로 다운로드.
263
252
 
264
- Bash:
265
- node "[FIGMA_SCRIPT]" images {fileKey} {섹션.nodeId} --out=images/{feature}/ --depth=10
253
+ #### b. 생성물 적용
266
254
 
267
- 검증: result.total = refs.size (누락 0)
268
- 전부 완료해야 c 단계로 진행.
269
255
  ```
256
+ render 출력물을 프로젝트에 적용:
270
257
 
271
- #### c. 클래스 매핑 테이블 생성
258
+ 1. 이미지 복사:
259
+ /tmp/{feature}-{section}/images/* → static/images/{feature}/
272
260
 
273
- ```
274
- SCSS 작성 전에 반드시 매핑 테이블을 먼저 출력한다.
275
- 테이블 없이 SCSS를 작성하지 않는다.
276
-
277
- 1. Phase 1 컴포넌트의 클래스 목록을 Read로 수집
278
- 2. 트리의 name + css 속성을 분석
279
- 3. 매핑 테이블 출력:
280
-
281
- ┌─────────────────────┬──────────────────┬────────────────────────────────────┐
282
- │ Phase 1 클래스 │ 트리 노드 name │ 추출된 CSS 값 │
283
- ├─────────────────────┼──────────────────┼────────────────────────────────────┤
284
- │ .kidSection │ KID (root) │ flex, column, gap:32px, pad:48px │
285
- │ .kidBg │ BG │ absolute, 720x800 │
286
- │ .kidLoginBtn │ Btn_Login │ flex, border, shadow, 640x148 │
287
- │ .kidLoginBtnText │ (TEXT 노드) │ fontSize:36px, color:#fff, w:700 │
288
- │ .kidDivider │ Divider │ 640x1 │
289
- │ .kidSteamLink │ steam_account │ fontSize:24px, w:600 │
290
- │ .kidSteamNote │ (하위 TEXT) │ fontSize:20px, color:#dadce3 │
291
- └─────────────────────┴──────────────────┴────────────────────────────────────┘
292
-
293
- 매핑 기준:
294
- name 일치 → 직접 매핑
295
- name 없음 → 트리 위치/text 내용으로 판단
296
- Phase 1에 없는 요소 → 클래스 신규 추가 (template에도 반영)
297
- 트리에 없는 클래스 → 스타일 없이 유지
298
- ```
261
+ 2. SCSS 적용:
262
+ {section}.scss를 읽고 scaleFactor 적용하여 프로젝트 SCSS에 Write:
263
+ styles/{feature}/layout/_{section}.scss position, display, flex, width, height
264
+ styles/{feature}/components/_{section}.scss ← font, color, border, shadow
265
+ styles/{feature}/_tokens.scss 추가 (primitive/semantic, vibe.figma.convert 참조)
266
+ index.scss에 섹션 @import 추가.
299
267
 
300
- #### d. SCSS 작성
268
+ 3. template 업데이트:
269
+ {section}.html을 읽고 Phase 1 컴포넌트에 반영:
270
+ - HTML 구조를 프로젝트 스택으로 변환 (class 유지)
271
+ - 이미지 경로를 static/images/{feature}/ 로 교체
272
+ - Phase 1 기능 요소(v-for, @click, v-if, $emit) 재배치
273
+ - script(JSDoc, 인터페이스, 핸들러) 보존
301
274
 
302
- ```
303
- 매핑 테이블의 각 행에 대해, 트리의 css 속성을 SCSS로 작성.
304
- CSS 값은 트리에서 이미 추출되어 있으므로 변환 불필요 — 그대로 사용.
305
-
306
- scaleFactor 적용:
307
- px 값 → × scaleFactor (font-size, padding, margin, gap, width, height, border-radius)
308
- 적용 안 함 → color, opacity, font-weight, z-index, line-height(단위 없음), % 값
309
-
310
- 출력 파일:
311
- styles/{feature}/layout/_{section}.scss
312
- → position, display, flex, width, height, padding, overflow, z-index, background-image
313
- styles/{feature}/components/_{section}.scss
314
- → font-size, font-weight, color, line-height, letter-spacing, text-align,
315
- border, border-radius, box-shadow, opacity
316
- styles/{feature}/_tokens.scss
317
- → 새로 발견된 색상/폰트/스페이싱 토큰 추가
318
-
319
- BG 레이어 패턴 (트리에서 position:absolute + 이미지 fill):
320
- .{section}Bg → position: absolute; inset: 0; z-index: 0;
321
- .{section}Content → position: relative; z-index: 1;
322
-
323
- index.scss에 새 섹션 @import 추가.
275
+ 4. 스크린샷 참조:
276
+ {section}-screenshot.png과 비교하면서 작업
324
277
  ```
325
278
 
326
- #### e. template 업데이트
327
-
328
- ```
329
- Phase 1 컴포넌트의 template을 트리 구조 기반으로 리팩토링.
330
- script(JSDoc, 인터페이스, 목 데이터, 핸들러)는 보존.
331
-
332
- 1. 트리의 HTML 구조를 프로젝트 스택으로 변환:
333
- FRAME → <div>, TEXT → <p>/<span>, VECTOR/RECTANGLE with imageRef → <img>
334
-
335
- 2. 이미지 경로를 imageMap으로 교체:
336
- imageRef "abc123" → src="/images/{feature}/abc123.png"
337
-
338
- 3. BG 레이어 구조 적용:
339
- .{section}Bg div (배경) + .{section}Content div (콘텐츠)
340
-
341
- 4. Phase 1 기능 요소 재배치:
342
- v-for, @click, v-if, $emit 등을 새 구조의 적절한 위치에 배치
343
-
344
- 5. 접근성:
345
- 장식 이미지 (BG 내) → alt="" aria-hidden="true"
346
- 콘텐츠 이미지 → alt="설명적 텍스트"
347
-
348
- 컴포넌트에 <style> 블록 없음. 스타일은 전부 외부 SCSS.
349
- ```
350
-
351
- #### f. 섹션 검증
279
+ #### c. 섹션 검증
352
280
 
353
281
  ```
354
282
  Grep 체크:
@@ -356,8 +284,11 @@ Grep 체크:
356
284
  □ "<style" in 컴포넌트 파일 → 0건
357
285
 
358
286
  Read 체크:
359
- □ 외부 SCSS 파일에 font-size, color 존재 (브라우저 기본 스타일 방지)
360
- □ 이미지 파일 = imageRef 수 (누락 0)
287
+ □ 외부 SCSS 파일에 font-size, color 존재
288
+ □ 이미지 파일 존재 + 0byte 없음
289
+
290
+ 스크린샷 비교:
291
+ □ {section}-screenshot.png vs dev 서버 → 주요 차이 없음
361
292
 
362
293
  실패 → 수정 → 재검증
363
294
  ```
@@ -74,8 +74,8 @@ tier: standard
74
74
  }
75
75
 
76
76
  .heroParticipation {
77
- font-size: t.$text-sub; // 24px × 0.75 = 18px
78
- color: t.$color-white;
77
+ font-size: t.$font-size-md; // 24px × 0.667 = 16px
78
+ color: t.$color-text-primary; // semantic → $color-white
79
79
  line-height: 1.4;
80
80
  text-align: center;
81
81
  white-space: nowrap;
@@ -91,35 +91,58 @@ components/ → font-size, font-weight, color, line-height, letter-spacing,
91
91
  text-align, border, border-radius, box-shadow, opacity
92
92
  ```
93
93
 
94
- ### _tokens.scss 업데이트
94
+ ### _tokens.scss 구조 (primitive/semantic 분리)
95
95
 
96
96
  ```
97
- 섹션을 처리할 때마다 새로운 고유값을 토큰에 추가:
98
-
97
+ // ─── Primitive (Figma 원시 값) ────────────────
99
98
  // Colors
100
- $color-white: #FFFFFF;
101
- $color-bg-dark: #0A1628;
102
- $color-heading: #1B3A1D;
103
- $color-text-body: #333333;
104
- $color-period-label: #003879;
105
- $color-grayscale-950: #171716;
106
-
107
- // Typography (Figma px × scaleFactor)
108
- $text-hero: 36px; // 48 × 0.75
109
- $text-sub: 18px; // 24 × 0.75
110
- $text-body: 16px; // 21 × 0.75
111
- $text-period: 21px; // 28 × 0.75
99
+ $color-white: #ffffff;
100
+ $color-black: #000000;
101
+ $color-navy-dark: #0a1628;
102
+ $color-navy-medium: #00264a;
112
103
 
113
104
  // Font families
114
105
  $font-pretendard: 'Pretendard', sans-serif;
115
106
  $font-roboto-condensed: 'Roboto Condensed', sans-serif;
116
107
 
117
- // Spacing
118
- $space-section: 98px; // 130 × 0.75
119
- $space-content: 18px; // 24 × 0.75
120
-
121
- // Breakpoints
122
- $bp-pc: 1024px;
108
+ // Font sizes (scaled)
109
+ $font-size-xs: 11px; // 16 × 0.667
110
+ $font-size-sm: 13px; // 20 × 0.667
111
+ $font-size-md: 16px; // 24 × 0.667
112
+ $font-size-lg: 19px; // 28 × 0.667
113
+
114
+ // Font weights
115
+ $font-weight-regular: 400;
116
+ $font-weight-medium: 500;
117
+ $font-weight-bold: 700;
118
+
119
+ // Spacing (scaled)
120
+ $space-xs: 5px;
121
+ $space-sm: 11px;
122
+ $space-md: 16px;
123
+ $space-lg: 21px;
124
+
125
+ // ─── Semantic (용도별) ────────────────────────
126
+ // Text
127
+ $color-text-primary: $color-white;
128
+ $color-text-secondary: #dadce3;
129
+ $color-text-label: #003879;
130
+ $color-text-link: #419bd3;
131
+
132
+ // Background
133
+ $color-bg-primary: $color-navy-dark;
134
+ $color-bg-section: $color-navy-medium;
135
+
136
+ // Border
137
+ $color-border-primary: #203f6c;
138
+
139
+ // Breakpoint
140
+ $bp-desktop: 1024px;
141
+
142
+ 규칙:
143
+ - primitive: 고유 값 (hex, 폰트명, px)
144
+ - semantic: primitive 참조로 용도별 이름 ($color-text-primary: $color-white)
145
+ - 같은 값 중복 금지 — 기존 토큰 재사용
123
146
  ```
124
147
 
125
148
  ### CSS 변수 패턴 처리