@su-record/vibe 2.8.33 → 2.8.34
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
@@ -68,16 +68,15 @@ tier: standard
|
|
|
68
68
|
|
|
69
69
|
```
|
|
70
70
|
URL에서 fileKey, nodeId 추출
|
|
71
|
-
getTree(fileKey, nodeId, depth=2) → 프레임 목록
|
|
72
71
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
→
|
|
93
|
+
→ node "[FIGMA_SCRIPT]" screenshot으로 시각 파악
|
|
95
94
|
→ 또는 depth 높여서 하위 분할 조회
|
|
96
95
|
```
|
|
97
96
|
|
|
@@ -226,7 +225,8 @@ SCSS 파일 기본 내용 Write:
|
|
|
226
225
|
|
|
227
226
|
### 2-2. 섹션 루프
|
|
228
227
|
|
|
229
|
-
|
|
228
|
+
**섹션별로 a→f 순서는 지키되, 섹션 간 병렬 처리 허용.**
|
|
229
|
+
**단, 첫 번째 섹션(Hero)은 단독 완료 후 나머지를 병렬로.**
|
|
230
230
|
|
|
231
231
|
#### a. 노드 트리 + CSS 추출
|
|
232
232
|
|
|
@@ -314,7 +314,58 @@ scaleFactor 적용:
|
|
|
314
314
|
→ font-size, font-weight, color, line-height, letter-spacing, text-align,
|
|
315
315
|
border, border-radius, box-shadow, opacity
|
|
316
316
|
styles/{feature}/_tokens.scss
|
|
317
|
-
→
|
|
317
|
+
→ primitive/semantic 구조로 토큰 관리 (아래 참조)
|
|
318
|
+
|
|
319
|
+
_tokens.scss 구조 (primitive/semantic 분리):
|
|
320
|
+
// ─── Primitive (Figma 원시 값) ────────────────
|
|
321
|
+
// Colors
|
|
322
|
+
$color-white: #ffffff;
|
|
323
|
+
$color-navy-dark: #0a1628;
|
|
324
|
+
$color-purple-500: #604fed;
|
|
325
|
+
|
|
326
|
+
// Font families
|
|
327
|
+
$font-pretendard: 'Pretendard', sans-serif;
|
|
328
|
+
$font-roboto-condensed: 'Roboto Condensed', sans-serif;
|
|
329
|
+
|
|
330
|
+
// Font sizes (scaled)
|
|
331
|
+
$font-size-xs: 11px; // 16 × 0.667
|
|
332
|
+
$font-size-sm: 13px; // 20 × 0.667
|
|
333
|
+
$font-size-md: 16px; // 24 × 0.667
|
|
334
|
+
$font-size-lg: 19px; // 28 × 0.667
|
|
335
|
+
|
|
336
|
+
// Font weights
|
|
337
|
+
$font-weight-regular: 400;
|
|
338
|
+
$font-weight-bold: 700;
|
|
339
|
+
|
|
340
|
+
// Spacing (scaled)
|
|
341
|
+
$space-xs: 5px;
|
|
342
|
+
$space-sm: 11px;
|
|
343
|
+
$space-md: 16px;
|
|
344
|
+
$space-lg: 21px;
|
|
345
|
+
|
|
346
|
+
// ─── Semantic (용도별) ────────────────────────
|
|
347
|
+
// Text
|
|
348
|
+
$color-text-primary: $color-white;
|
|
349
|
+
$color-text-secondary: #dadce3;
|
|
350
|
+
$color-text-label: #003879;
|
|
351
|
+
$color-text-link: #419bd3;
|
|
352
|
+
|
|
353
|
+
// Background
|
|
354
|
+
$color-bg-primary: $color-navy-dark;
|
|
355
|
+
$color-bg-section: #00264a;
|
|
356
|
+
$color-bg-login: rgba(14, 35, 62, 0.8);
|
|
357
|
+
|
|
358
|
+
// Border
|
|
359
|
+
$color-border-primary: #203f6c;
|
|
360
|
+
|
|
361
|
+
// Breakpoint
|
|
362
|
+
$bp-desktop: 1024px;
|
|
363
|
+
|
|
364
|
+
규칙:
|
|
365
|
+
- primitive: Figma에서 추출한 고유 값 (색상 hex, 폰트, 크기)
|
|
366
|
+
- semantic: primitive를 참조하여 용도별 이름 부여 ($color-text-primary: $color-white)
|
|
367
|
+
- 섹션 처리할 때마다 새 값 발견 시 적절한 카테고리에 추가
|
|
368
|
+
- 같은 값이 이미 있으면 기존 토큰 재사용 (중복 금지)
|
|
318
369
|
|
|
319
370
|
BG 레이어 패턴 (트리에서 position:absolute + 이미지 fill):
|
|
320
371
|
.{section}Bg → position: absolute; inset: 0; z-index: 0;
|
|
@@ -74,8 +74,8 @@ tier: standard
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
.heroParticipation {
|
|
77
|
-
font-size: t.$
|
|
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: #
|
|
101
|
-
$color-
|
|
102
|
-
$color-
|
|
103
|
-
$color-
|
|
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
|
-
//
|
|
118
|
-
$
|
|
119
|
-
$
|
|
120
|
-
|
|
121
|
-
//
|
|
122
|
-
|
|
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 변수 패턴 처리
|