@su-record/vibe 2.8.53 → 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.
- package/CLAUDE.md +1 -0
- package/README.ko.md +43 -128
- package/README.md +43 -128
- package/agents/teams/debug-team.md +70 -0
- package/agents/teams/dev-team.md +88 -0
- package/agents/teams/docs-team.md +80 -0
- package/agents/teams/figma/figma-analyst.md +52 -0
- package/agents/teams/figma/figma-architect.md +112 -0
- package/agents/teams/figma/figma-auditor.md +82 -0
- package/agents/teams/figma/figma-builder.md +72 -0
- package/agents/teams/figma-team.md +85 -0
- package/agents/teams/fullstack-team.md +83 -0
- package/agents/teams/lite-team.md +69 -0
- package/agents/teams/migration-team.md +78 -0
- package/agents/teams/refactor-team.md +94 -0
- package/agents/teams/research-team.md +86 -0
- package/agents/teams/review-debate-team.md +125 -0
- package/agents/teams/security-team.md +81 -0
- package/commands/vibe.review.md +1 -63
- package/commands/vibe.run.md +8 -376
- package/commands/vibe.spec.md +1 -59
- package/commands/vibe.spec.review.md +1 -45
- package/dist/cli/postinstall/main.d.ts.map +1 -1
- package/dist/cli/postinstall/main.js +40 -66
- package/dist/cli/postinstall/main.js.map +1 -1
- package/dist/infra/lib/CostAccumulator.d.ts +58 -0
- package/dist/infra/lib/CostAccumulator.d.ts.map +1 -0
- package/dist/infra/lib/CostAccumulator.js +131 -0
- package/dist/infra/lib/CostAccumulator.js.map +1 -0
- package/hooks/scripts/figma-refine.js +315 -0
- package/hooks/scripts/figma-to-scss.js +394 -0
- package/hooks/scripts/figma-validate.js +353 -0
- package/package.json +1 -1
- package/skills/vibe.figma/SKILL.md +92 -24
- package/skills/vibe.figma/templates/component-spec.md +168 -0
- package/skills/vibe.figma.convert/SKILL.md +39 -3
- package/skills/vibe.figma.convert/rubrics/conversion-rules.md +12 -0
- package/skills/vibe.figma.extract/SKILL.md +29 -1
- package/skills/vibe.figma.extract/rubrics/image-rules.md +15 -3
|
@@ -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
|
@@ -84,7 +84,7 @@ URL 분류 (자동):
|
|
|
84
84
|
ROOT name에 "MO" → 모바일, "PC" → 데스크탑
|
|
85
85
|
|
|
86
86
|
스토리보드 분석:
|
|
87
|
-
depth=
|
|
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:
|
|
123
|
+
## Phase 3: 데이터 정제 ← Synthesis (BP별 독립)
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
**리더가 두 tree를 모두 이해한 상태에서 diff를 추출해야 품질 보장.**
|
|
125
|
+
**각 BP의 tree.json을 섹션별로 분할 + 정제한다.**
|
|
126
|
+
**MO↔PC 매칭(반응형)은 이 단계에서 하지 않는다.**
|
|
128
127
|
|
|
129
|
-
###
|
|
128
|
+
### 핵심 원칙
|
|
130
129
|
|
|
131
130
|
```
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
⛔ BP별 독립 정제. MO와 PC를 섞지 않는다.
|
|
132
|
+
⛔ 정제된 JSON은 Phase 4의 유일한 입력이다.
|
|
133
|
+
⛔ 섹션별 전체 하위 트리(children 재귀)를 반드시 포함해야 한다.
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 출력
|
|
135
137
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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:
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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,
|
|
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 인터페이스 정의 필수
|