@su-record/vibe 2.9.1 → 2.9.2

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.
@@ -0,0 +1,353 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * figma-validate.js — SCSS vs sections.json 대조 검증
5
+ *
6
+ * Usage:
7
+ * node figma-validate.js <scss-dir> <sections.json> [--section=<name>]
8
+ *
9
+ * 검증 항목:
10
+ * 1. SCSS의 모든 CSS 속성이 sections.json에 근거하는가
11
+ * 2. sections.json의 CSS 속성이 SCSS에 누락되지 않았는가
12
+ * 3. 금지 패턴 감지 (커스텀 함수, aspect-ratio 등)
13
+ * 4. 이미지 파일명 kebab-case 확인
14
+ *
15
+ * 출력: JSON { status, errors[], summary }
16
+ */
17
+
18
+ import fs from 'fs';
19
+ import path from 'path';
20
+
21
+ // ─── Helpers ────────────────────────────────────────────────────────
22
+
23
+ function camelToKebab(str) {
24
+ return str.replace(/([A-Z])/g, '-$1').toLowerCase();
25
+ }
26
+
27
+ function parseSCSS(scssContent) {
28
+ // 간이 SCSS 파서: 셀렉터 → CSS 속성 맵 추출
29
+ const blocks = {};
30
+ let currentSelector = null;
31
+ const lines = scssContent.split('\n');
32
+
33
+ for (const line of lines) {
34
+ const trimmed = line.trim();
35
+
36
+ // 빈 줄, 코멘트 스킵
37
+ if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('/*')) continue;
38
+
39
+ // 셀렉터 열기
40
+ const selectorMatch = trimmed.match(/^([.&][a-zA-Z0-9_-][a-zA-Z0-9_-]*(?:__[a-zA-Z0-9_-]+(?:-[a-zA-Z0-9_-]+)*)?)\s*\{/);
41
+ if (selectorMatch) {
42
+ currentSelector = selectorMatch[1];
43
+ if (!blocks[currentSelector]) blocks[currentSelector] = {};
44
+ continue;
45
+ }
46
+
47
+ // 블록 닫기
48
+ if (trimmed === '}') {
49
+ currentSelector = null;
50
+ continue;
51
+ }
52
+
53
+ // CSS 속성
54
+ if (currentSelector && trimmed.includes(':') && trimmed.endsWith(';')) {
55
+ const colonIdx = trimmed.indexOf(':');
56
+ const prop = trimmed.slice(0, colonIdx).trim();
57
+ const value = trimmed.slice(colonIdx + 1).replace(/;$/, '').trim();
58
+ blocks[currentSelector][prop] = value;
59
+ }
60
+ }
61
+
62
+ return blocks;
63
+ }
64
+
65
+ // ─── Forbidden Pattern Detection ────────────────────────────────────
66
+
67
+ const FORBIDDEN_PATTERNS = [
68
+ { pattern: /@function\s/, id: 'custom-function', msg: '@function 자체 정의 금지' },
69
+ { pattern: /@mixin\s/, id: 'custom-mixin', msg: '@mixin 자체 정의 금지 (기존 @use만 허용)' },
70
+ { pattern: /aspect-ratio\s*:/, id: 'aspect-ratio', msg: 'aspect-ratio는 tree.json에 없는 속성' },
71
+ { pattern: /clamp\s*\(/, id: 'clamp', msg: 'clamp()는 font-size 외 사용 금지' },
72
+ { pattern: /\d+vw/, id: 'vw-unit', msg: 'vw 단위 사용 금지 (스태틱 구현)' },
73
+ { pattern: /@media\s/, id: 'media-query', msg: '@media 금지 (스태틱 구현)' },
74
+ { pattern: /@include\s/, id: 'include', msg: '@include 사용 시 기존 프로젝트 믹스인만 허용' },
75
+ ];
76
+
77
+ function detectForbidden(scssContent, filePath) {
78
+ const errors = [];
79
+ const lines = scssContent.split('\n');
80
+
81
+ for (let i = 0; i < lines.length; i++) {
82
+ const line = lines[i];
83
+ for (const { pattern, id, msg } of FORBIDDEN_PATTERNS) {
84
+ if (pattern.test(line)) {
85
+ // clamp in font-size is allowed
86
+ if (id === 'clamp' && line.includes('font-size')) continue;
87
+ errors.push({
88
+ priority: 'P1',
89
+ file: filePath,
90
+ line: i + 1,
91
+ type: `forbidden-${id}`,
92
+ actual: line.trim(),
93
+ message: msg
94
+ });
95
+ }
96
+ }
97
+ }
98
+
99
+ return errors;
100
+ }
101
+
102
+ // ─── CSS Value Comparison ───────────────────────────────────────────
103
+
104
+ function collectCSSFromTree(node, sectionClass, parentPath, result) {
105
+ const css = node.css || {};
106
+ const children = node.children || [];
107
+ const name = node.name || '';
108
+ const type = node.type || '';
109
+
110
+ // BG 프레임 스킵
111
+ const nameLower = name.toLowerCase();
112
+ if (nameLower === 'bg' || nameLower.endsWith('-bg') || nameLower.startsWith('bg-')) return;
113
+
114
+ // 클래스명 재현 (figma-to-scss.js와 동일 로직)
115
+ let cls = name
116
+ .replace(/[^a-zA-Z0-9\s_-]/g, '')
117
+ .replace(/[\s_]+/g, '-')
118
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
119
+ .toLowerCase()
120
+ .replace(/-+/g, '-')
121
+ .replace(/^-|-$/g, '') || null;
122
+
123
+ if (!cls) {
124
+ const idx = parentPath ? parentPath.split('-').length : 0;
125
+ if (type === 'TEXT') cls = `text-${idx}`;
126
+ else if (type === 'VECTOR') cls = `vector-${idx}`;
127
+ else if (type === 'RECTANGLE') cls = `rect-${idx}`;
128
+ else cls = `el-${idx}`;
129
+ }
130
+
131
+ const currentPath = parentPath ? `${parentPath}-${cls}` : cls;
132
+ const selector = `.${sectionClass}__${currentPath}`;
133
+
134
+ // CSS 속성 저장 (kebab 변환)
135
+ if (Object.keys(css).length > 0) {
136
+ const kebabCSS = {};
137
+ for (const [prop, value] of Object.entries(css)) {
138
+ if (prop.startsWith('_')) continue;
139
+ kebabCSS[camelToKebab(prop)] = String(value);
140
+ }
141
+ result[selector] = kebabCSS;
142
+ }
143
+
144
+ // 자식 재귀
145
+ const seenClasses = new Set();
146
+ for (const child of children) {
147
+ const childName = child.name || '';
148
+ const childLower = childName.toLowerCase();
149
+ if (childLower === 'bg' || childLower.endsWith('-bg') || childLower.startsWith('bg-')) continue;
150
+
151
+ let childCls = childName
152
+ .replace(/[^a-zA-Z0-9\s_-]/g, '')
153
+ .replace(/[\s_]+/g, '-')
154
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
155
+ .toLowerCase()
156
+ .replace(/-+/g, '-')
157
+ .replace(/^-|-$/g, '') || null;
158
+ if (!childCls) childCls = `${child.type?.toLowerCase() || 'el'}-0`;
159
+
160
+ if (seenClasses.has(childCls)) continue;
161
+ seenClasses.add(childCls);
162
+ collectCSSFromTree(child, sectionClass, currentPath, result);
163
+ }
164
+ }
165
+
166
+ function compareCSSValues(scssBlocks, treeCSS, filePath) {
167
+ const errors = [];
168
+
169
+ // SCSS에 있지만 tree에 없는 속성 (임의 추가)
170
+ for (const [selector, props] of Object.entries(scssBlocks)) {
171
+ if (!selector.startsWith('.')) continue;
172
+ const treeProps = treeCSS[selector];
173
+
174
+ for (const [prop, value] of Object.entries(props)) {
175
+ // background-image url, background-size 등은 이미지 처리에서 추가됨
176
+ if (prop === 'background-image' || prop === 'background-size' ||
177
+ prop === 'background-position' || prop === 'background-repeat') continue;
178
+ // imageRef 주석 스킵
179
+ if (prop.startsWith('//')) continue;
180
+
181
+ if (!treeProps || !(prop in treeProps)) {
182
+ errors.push({
183
+ priority: 'P2',
184
+ file: filePath,
185
+ type: 'extra-property',
186
+ selector,
187
+ property: prop,
188
+ actual: value,
189
+ message: `SCSS에 있지만 sections.json에 없는 속성: ${prop}: ${value}`
190
+ });
191
+ } else if (treeProps[prop] !== value) {
192
+ errors.push({
193
+ priority: 'P1',
194
+ file: filePath,
195
+ type: 'value-mismatch',
196
+ selector,
197
+ property: prop,
198
+ expected: treeProps[prop],
199
+ actual: value,
200
+ message: `값 불일치: ${prop} expected="${treeProps[prop]}" actual="${value}"`
201
+ });
202
+ }
203
+ }
204
+ }
205
+
206
+ // tree에 있지만 SCSS에 없는 속성 (누락)
207
+ for (const [selector, props] of Object.entries(treeCSS)) {
208
+ const scssProps = scssBlocks[selector];
209
+ if (!scssProps) {
210
+ errors.push({
211
+ priority: 'P1',
212
+ file: filePath,
213
+ type: 'missing-selector',
214
+ selector,
215
+ message: `sections.json에 있지만 SCSS에 없는 셀렉터: ${selector}`
216
+ });
217
+ continue;
218
+ }
219
+ for (const [prop] of Object.entries(props)) {
220
+ if (!(prop in scssProps)) {
221
+ errors.push({
222
+ priority: 'P1',
223
+ file: filePath,
224
+ type: 'missing-property',
225
+ selector,
226
+ property: prop,
227
+ expected: props[prop],
228
+ message: `SCSS에 누락된 속성: ${selector} { ${prop}: ${props[prop]} }`
229
+ });
230
+ }
231
+ }
232
+ }
233
+
234
+ return errors;
235
+ }
236
+
237
+ // ─── Image Filename Check ───────────────────────────────────────────
238
+
239
+ function checkImageFilenames(scssContent, filePath) {
240
+ const errors = [];
241
+ const urlRegex = /url\(['"]?([^'")\s]+)['"]?\)/g;
242
+ let match;
243
+
244
+ while ((match = urlRegex.exec(scssContent)) !== null) {
245
+ const imgPath = match[1];
246
+ const filename = path.basename(imgPath, path.extname(imgPath));
247
+
248
+ // 해시 파일명 감지 (16자 이상 hex)
249
+ if (/^[0-9a-f]{16,}$/.test(filename)) {
250
+ errors.push({
251
+ priority: 'P1',
252
+ file: filePath,
253
+ type: 'hash-filename',
254
+ actual: imgPath,
255
+ message: `해시 파일명 금지: ${imgPath} → kebab-case로 변경 필요`
256
+ });
257
+ }
258
+ }
259
+
260
+ return errors;
261
+ }
262
+
263
+ // ─── Main ───────────────────────────────────────────────────────────
264
+
265
+ function main() {
266
+ const args = process.argv.slice(2);
267
+ if (args.length < 2) {
268
+ console.error('Usage: node figma-validate.js <scss-dir> <sections.json> [--section=<name>]');
269
+ process.exit(1);
270
+ }
271
+
272
+ const scssDir = args[0];
273
+ const sectionsFile = args[1];
274
+ let sectionFilter = '';
275
+
276
+ for (const arg of args.slice(2)) {
277
+ if (arg.startsWith('--section=')) sectionFilter = arg.slice(10);
278
+ }
279
+
280
+ // 입력 읽기
281
+ const data = JSON.parse(fs.readFileSync(sectionsFile, 'utf-8'));
282
+ let sections = data.sections || [];
283
+
284
+ if (sectionFilter) {
285
+ sections = sections.filter(s => s.name === sectionFilter);
286
+ }
287
+
288
+ const allErrors = [];
289
+
290
+ for (const section of sections) {
291
+ const sectionClass = section.name
292
+ .replace(/[^a-zA-Z0-9\s_-]/g, '')
293
+ .replace(/[\s_]+/g, '-')
294
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
295
+ .toLowerCase()
296
+ .replace(/-+/g, '-')
297
+ .replace(/^-|-$/g, '');
298
+
299
+ const scssFile = path.join(scssDir, `_${sectionClass}.scss`);
300
+ if (!fs.existsSync(scssFile)) {
301
+ allErrors.push({
302
+ priority: 'P1',
303
+ type: 'missing-file',
304
+ expected: scssFile,
305
+ message: `SCSS 파일 없음: ${scssFile}`
306
+ });
307
+ continue;
308
+ }
309
+
310
+ const scssContent = fs.readFileSync(scssFile, 'utf-8');
311
+
312
+ // 1. 금지 패턴 검사
313
+ allErrors.push(...detectForbidden(scssContent, scssFile));
314
+
315
+ // 2. 이미지 파일명 검사
316
+ allErrors.push(...checkImageFilenames(scssContent, scssFile));
317
+
318
+ // 3. CSS 값 대조
319
+ const scssBlocks = parseSCSS(scssContent);
320
+ const treeCSS = {};
321
+
322
+ // 루트 섹션 CSS
323
+ const rootSelector = `.${sectionClass}`;
324
+ if (section.css && Object.keys(section.css).length > 0) {
325
+ treeCSS[rootSelector] = {};
326
+ for (const [prop, value] of Object.entries(section.css)) {
327
+ if (!prop.startsWith('_')) treeCSS[rootSelector][camelToKebab(prop)] = String(value);
328
+ }
329
+ }
330
+
331
+ // 자식 CSS 재귀 수집
332
+ for (const child of (section.children || [])) {
333
+ collectCSSFromTree(child, sectionClass, '', treeCSS);
334
+ }
335
+
336
+ allErrors.push(...compareCSSValues(scssBlocks, treeCSS, scssFile));
337
+ }
338
+
339
+ // 결과 출력
340
+ const p1 = allErrors.filter(e => e.priority === 'P1').length;
341
+ const p2 = allErrors.filter(e => e.priority === 'P2').length;
342
+
343
+ const result = {
344
+ status: p1 === 0 ? 'PASS' : 'FAIL',
345
+ errors: allErrors,
346
+ summary: { p1, p2, total: allErrors.length }
347
+ };
348
+
349
+ console.log(JSON.stringify(result, null, 2));
350
+ process.exit(p1 > 0 ? 1 : 0);
351
+ }
352
+
353
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@su-record/vibe",
3
- "version": "2.9.1",
3
+ "version": "2.9.2",
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",
@@ -84,7 +84,7 @@ URL 분류 (자동):
84
84
  ROOT name에 "MO" → 모바일, "PC" → 데스크탑
85
85
 
86
86
  스토리보드 분석:
87
- depth=2로 프레임 수집 → name 패턴으로 분류
87
+ depth=3으로 프레임 수집 → name 패턴으로 분류
88
88
  SPEC(기능정의서) → CONFIG(해상도) → PAGE(메인 섹션) → SHARED(공통)
89
89
  PDF/이미지도 동일 구조 추출
90
90
 
@@ -120,25 +120,63 @@ MO/PC 동시 추출 (각각 독립 워커):
120
120
 
121
121
  ---
122
122
 
123
- ## Phase 3: 리매핑 ← Synthesis (순차, 리더 필수)
123
+ ## Phase 3: 데이터 정제 ← Synthesis (BP별 독립)
124
124
 
125
- **MO + PC 있을 때만 실행. 단일 BP면 스킵.**
126
- **코디네이터 패턴: 리더가 직접 MO↔PC 매칭. 워커 위임 금지.**
127
- **리더가 두 tree를 모두 이해한 상태에서 diff를 추출해야 품질 보장.**
125
+ **각 BP의 tree.json을 섹션별로 분할 + 정제한다.**
126
+ **MO↔PC 매칭(반응형)은 단계에서 하지 않는다.**
128
127
 
129
- ### MO↔PC 반응형 매칭
128
+ ### 핵심 원칙
130
129
 
131
130
  ```
132
- 1. 섹션 매칭: 1depth name 기준 (완전일치 → prefix → 순서)
133
- 2. 노드 매칭: 재귀적 name 매칭 → CSS diff 추출
134
- 3. diff: 같은 유지, 다른 값만 @media 오버라이드
131
+ BP별 독립 정제. MO와 PC를 섞지 않는다.
132
+ 정제된 JSON은 Phase 4의 유일한 입력이다.
133
+ 섹션별 전체 하위 트리(children 재귀)를 반드시 포함해야 한다.
134
+ ```
135
+
136
+ ### 출력
135
137
 
136
- 출력 → /tmp/{feature}/remapped.json:
137
- sections:
138
- - name: Hero
139
- mo: { nodeId, css, children }
140
- pcDiff: { css: { 차이만 }, children }
141
- images: { mo: 'bg/hero-bg.webp', pc: 'bg/hero-bg-pc.webp' }
138
+ ```
139
+ /tmp/{feature}/
140
+ mo-main/
141
+ sections.json ← MO 정제 결과
142
+ pc-main/
143
+ sections.json ← PC 정제 결과
144
+
145
+ sections.json 구조:
146
+ {
147
+ meta: { feature, designWidth, bp(해당 BP) },
148
+ sections: [
149
+ {
150
+ name: "Hero",
151
+ nodeId, name, type, size, css,
152
+ text, // TEXT 노드만
153
+ imageRef, // 이미지 fill
154
+ fills, // 다중 fill (2개 이상)
155
+ layoutSizingH, // HUG/FILL/FIXED
156
+ layoutSizingV,
157
+ children: [ // ⛔ 전체 하위 트리 재귀 — 잎 노드까지
158
+ { nodeId, name, type, size, css, children: [...] }
159
+ ],
160
+ images: {
161
+ bg: "bg/hero-bg.webp",
162
+ content: ["content/hero-title.webp"]
163
+ }
164
+ }
165
+ ]
166
+ }
167
+ ```
168
+
169
+ ### 노드 정제 규칙
170
+
171
+ ```
172
+ tree.json → sections.json 변환 시 정제:
173
+ 1. 크기 0px 노드 → 제거
174
+ 2. VECTOR 장식선 (w/h ≤ 2px) → 제거
175
+ 3. isMask 노드 → 제거
176
+ 4. BG 프레임 → children에서 분리, images.bg로 이동
177
+ 5. 벡터 글자 GROUP → children에서 분리, images.content에 추가
178
+ 6. 디자인 텍스트 (fills 다중/gradient, effects 있는 TEXT) → images.content에 추가
179
+ 7. 나머지 노드 → children에 유지 (CSS 포함, 재귀)
142
180
  ```
143
181
 
144
182
  ### 멀티 프레임 (같은 BP, 다른 페이지)
@@ -150,20 +188,43 @@ MO/PC 동시 추출 (각각 독립 워커):
150
188
 
151
189
  ---
152
190
 
153
- ## Phase 4: 순차 코드 생성 ← Implement (영역별)
191
+ ## Phase 4: BP별 스태틱 구현 ← Implement (BP별 순차)
154
192
 
155
193
  **→ vibe.figma.convert 스킬의 규칙을 따른다.**
156
- **코디네이터 패턴: 섹션 = 영역. 영역에 워커만. 충돌 방지.**
194
+ **⛔ MO 먼저 전체 구현 검증 통과 PC 구현. 반응형 변환은 하지 않는다.**
195
+ **⛔ CSS 값은 Figma 원본 px 그대로. vw 변환, clamp, @media 금지.**
157
196
 
158
197
  ```
159
- 병렬 금지. 섹션씩 순차:
160
- 1. tree.json에서 섹션 노드 Read
161
- 2. 이미지 vs HTML 판별 테이블 작성 (BLOCKING)
162
- 3. 기계적 매핑 + Claude 시맨틱 보강
163
- 4. 브라우저 확인 OK 다음 섹션
198
+ Phase 4A: MO 스태틱 구현
199
+ 입력: /tmp/{feature}/mo-main/sections.json
200
+ 병렬 금지. 섹션씩 순차:
201
+ 1. sections.json에서 해당 섹션 Read
202
+ 2. 이미지 vs HTML 판별 테이블 작성 (BLOCKING)
203
+ 3. figma-to-scss.js → SCSS 골격 자동 생성 (px 그대로)
204
+ 4. Claude: HTML 구조 + 시맨틱 태그 + 레이아웃 + 인터랙션
205
+ 5. figma-validate.js → SCSS vs sections.json 대조
206
+ ├─ PASS → 다음 섹션
207
+ └─ FAIL → 불일치 수정 → 5번 재실행 (P1=0 까지, 횟수 제한 없음)
208
+ → Phase 5 (MO 컴파일) → Phase 6 (MO 시각 검증)
209
+
210
+ Phase 4B: PC 스태틱 구현
211
+ 입력: /tmp/{feature}/pc-main/sections.json
212
+ MO와 동일한 프로세스
213
+ → Phase 5 (PC 컴파일) → Phase 6 (PC 시각 검증)
214
+
215
+ Phase 4C: 반응형 통합 (MO+PC 모두 검증 통과 후)
216
+ → 별도 플로우로 수립 (TODO)
217
+
218
+ Claude의 역할 (제한적):
219
+ ✅ 이미지 분류: BG / 콘텐츠 / 장식 / 벡터 글자
220
+ ✅ HTML 시맨틱: section/h1/p/button 태그 선택
221
+ ✅ 컴포넌트 분리: v-for 반복, 공유 컴포넌트
222
+ ✅ 인터랙션: @click, 상태 변수, 조건부 렌더링
223
+ ❌ SCSS CSS 값 수정 금지 (figma-to-scss.js 출력 그대로 사용)
224
+ ❌ vw 변환, clamp, @media, 커스텀 함수/믹스인 생성 금지
164
225
 
165
226
  SCSS Setup (첫 섹션 전):
166
- index.scss, _tokens.scss, _mixins.scss, _base.scss
227
+ index.scss, _tokens.scss, _base.scss
167
228
  토큰 매핑: project-tokens.json에서 기존 토큰 참조 → 매칭 안 되면 새 생성
168
229
 
169
230
  컴포넌트 매칭 (각 섹션 전):
@@ -191,12 +252,17 @@ SCSS Setup (첫 섹션 전):
191
252
  에러 시: 파싱 → 자동 수정 → 재체크
192
253
  3라운드 실패: 에러 목록을 사용자에게 보고 (Phase 6 진행 불가)
193
254
  완료 시: dev 서버 PID 보존 → Phase 6에서 사용
255
+
256
+ ⛔ Phase 5 통과 후 반드시 Phase 6 진입. "완료 요약" 출력 금지.
257
+ ⛔ Phase 6 없이 작업 완료 선언 금지.
194
258
  ```
195
259
 
196
260
  ---
197
261
 
198
- ## Phase 6: 시각 검증 루프 ← Verify (병렬)
262
+ ## Phase 6: 시각 검증 루프 ← Verify (병렬) ⛔ MANDATORY
199
263
 
264
+ **⛔ Phase 6은 선택이 아닌 필수. Phase 5 통과 즉시 자동 진입.**
265
+ **⛔ Phase 6 미실행 시 전체 작업은 "미완료" 상태.**
200
266
  **코디네이터 패턴: 독립 섹션별 검증을 워커로 병렬 실행 가능.**
201
267
 
202
268
  ```
@@ -212,4 +278,6 @@ SCSS Setup (첫 섹션 전):
212
278
 
213
279
  반응형: MO 검증 후 viewport 변경 → PC 스크린샷과 동일 루프
214
280
  종료: 브라우저 + dev 서버 정리
281
+
282
+ ⛔ Phase 6 완료 후에만 "완료 요약" 출력 허용.
215
283
  ```
@@ -0,0 +1,168 @@
1
+ # component-spec.json 템플릿
2
+
3
+ architect 에이전트가 sections.json을 분석하여 생성하는 설계서.
4
+ builder 에이전트는 이 설계서대로만 구현한다.
5
+
6
+ ## 생성 규칙
7
+
8
+ ```
9
+ 입력: /tmp/{feature}/{bp}-main/sections.json
10
+ 출력: /tmp/{feature}/{bp}-main/component-spec.json
11
+
12
+ ⛔ CSS 값을 결정하지 않는다 (figma-to-scss.js가 담당)
13
+ ⛔ SCSS 파일을 생성하지 않는다
14
+ ✅ HTML 구조, 태그 선택, 컴포넌트 분리, 이미지 분류만 결정
15
+ ```
16
+
17
+ ## 구조
18
+
19
+ ```json
20
+ {
21
+ "meta": {
22
+ "feature": "winter-pcbang",
23
+ "bp": "mo",
24
+ "designWidth": 720,
25
+ "stack": "nuxt2-vue2-scss"
26
+ },
27
+
28
+ "components": [
29
+ {
30
+ "name": "KidSection",
31
+ "sectionName": "KID",
32
+ "file": "components/{feature}/KidSection.vue",
33
+ "scssFile": "_kid.scss",
34
+ "tag": "section",
35
+ "id": "kid",
36
+
37
+ "bg": {
38
+ "image": "bg/kid-bg.webp",
39
+ "method": "css-background"
40
+ },
41
+
42
+ "elements": [
43
+ {
44
+ "class": "btn-login",
45
+ "tag": "button",
46
+ "role": "interactive",
47
+ "event": { "type": "click", "action": "emit('login')" },
48
+ "children": [
49
+ {
50
+ "class": "btn-login-btn-login",
51
+ "tag": "div",
52
+ "role": "layout"
53
+ },
54
+ {
55
+ "class": "btn-login-btn-login-krafton-id",
56
+ "tag": "img",
57
+ "role": "content-image",
58
+ "src": "content/kid-krafton-id.webp",
59
+ "alt": "KRAFTON ID 로그인"
60
+ }
61
+ ]
62
+ },
63
+ {
64
+ "class": "krafton-id",
65
+ "tag": "p",
66
+ "role": "text",
67
+ "text": "KRAFTON ID로 로그인하고 이벤트에 참여하세요!..."
68
+ },
69
+ {
70
+ "class": "steam-account",
71
+ "tag": "div",
72
+ "role": "layout",
73
+ "children": [
74
+ {
75
+ "class": "steam-account-frame-27161",
76
+ "tag": "a",
77
+ "role": "link",
78
+ "href": "#",
79
+ "children": [
80
+ {
81
+ "class": "steam-account-frame-27161-krafton-id-steam",
82
+ "tag": "span",
83
+ "role": "text",
84
+ "text": "KRAFTON ID Steam 연동 안내"
85
+ }
86
+ ]
87
+ },
88
+ {
89
+ "class": "steam-account-kakao-games-krafton-id",
90
+ "tag": "p",
91
+ "role": "text",
92
+ "text": "Kakao games는 게임 내에서만..."
93
+ }
94
+ ]
95
+ }
96
+ ],
97
+
98
+ "imageClassification": [
99
+ {
100
+ "node": "BG",
101
+ "decision": "bg",
102
+ "reason": "name='BG', 합성 배경, TEXT 자식 없음"
103
+ },
104
+ {
105
+ "node": "KRAFTON ID 로그인",
106
+ "decision": "content-image",
107
+ "reason": "RENDERED_IMAGE (디자인 텍스트, outline 효과)"
108
+ },
109
+ {
110
+ "node": "볼트_2",
111
+ "decision": "skip",
112
+ "reason": "5x5 장식 볼트, 동일 imageRef 4개 → CSS로 대체 가능"
113
+ }
114
+ ]
115
+ }
116
+ ],
117
+
118
+ "shared": [],
119
+
120
+ "tokens": {
121
+ "note": "figma-to-scss.js가 _tokens.scss 자동 생성. architect는 참조만."
122
+ }
123
+ }
124
+ ```
125
+
126
+ ## 판단 기준
127
+
128
+ ### tag 선택
129
+ | sections.json 조건 | tag |
130
+ |---|---|
131
+ | 섹션 루트 | `section` |
132
+ | type=TEXT + 제목 역할 (name에 title/heading) | `h2`~`h6` |
133
+ | type=TEXT + 설명 | `p` |
134
+ | type=TEXT + 라벨 | `span` / `strong` |
135
+ | name에 btn/button/CTA | `button` |
136
+ | name에 link/연동/안내 + href 가능 | `a` |
137
+ | type=RENDERED_IMAGE | `img` |
138
+ | imageRef 있음 + 콘텐츠 이미지 | `img` |
139
+ | INSTANCE 반복 2+ | 부모에 v-for 표시 |
140
+ | 나머지 FRAME/GROUP | `div` |
141
+
142
+ ### role 종류
143
+ | role | 의미 |
144
+ |---|---|
145
+ | `bg` | 배경 (CSS background-image) |
146
+ | `layout` | 레이아웃 컨테이너 |
147
+ | `text` | 텍스트 콘텐츠 |
148
+ | `content-image` | 콘텐츠 이미지 (img 태그) |
149
+ | `interactive` | 클릭/인터랙션 가능 |
150
+ | `link` | 네비게이션 링크 |
151
+ | `decoration` | 장식 (aria-hidden) |
152
+ | `list` | 반복 구조 (v-for) |
153
+ | `skip` | 무시 (너무 작거나 불필요) |
154
+
155
+ ### 이미지 분류 (imageClassification)
156
+ ⛔ 모든 이미지 관련 노드에 대해 반드시 분류 기록.
157
+ | decision | 조건 |
158
+ |---|---|
159
+ | `bg` | isBGFrame, TEXT 자식 없음 |
160
+ | `content-image` | RENDERED_IMAGE, 벡터 글자, 디자인 텍스트 |
161
+ | `asset` | imageRef 있는 아이콘/썸네일 |
162
+ | `skip` | 너무 작은 장식 (≤5px), CSS로 대체 가능 |
163
+ | `html` | TEXT 자식 포함, 인터랙티브, 동적 데이터 |
164
+
165
+ ### 공유 컴포넌트 (shared)
166
+ - 동일 구조 INSTANCE가 2+ 섹션에서 사용 → shared
167
+ - 3+ 사용 시 필수 분리
168
+ - Props/Slots 인터페이스 정의 필수