palette-mcp 1.0.0

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,1073 @@
1
+ import { saveFile, saveBinaryFile, saveMetadata, generateRequestId, getRequestFolderPath } from '../utils/request-manager.js';
2
+ import puppeteer from 'puppeteer';
3
+ export class CodeGenerator {
4
+ designSystemService;
5
+ constructor(designSystemService) {
6
+ this.designSystemService = designSystemService;
7
+ }
8
+ /**
9
+ * Figma 데이터에서 React 컴포넌트 생성
10
+ */
11
+ async generateReactComponent(figmaData, componentName) {
12
+ const imports = new Set();
13
+ const dependencies = new Set();
14
+ let componentCode = '';
15
+ // Figma 구조 분석 및 디자인 시스템 컴포넌트 매핑
16
+ const mappedComponents = this.mapFigmaToDesignSystem(figmaData.document, 'react');
17
+ // Generate imports
18
+ for (const mapping of mappedComponents) {
19
+ if (mapping.designSystemComponent) {
20
+ imports.add(`import { ${mapping.designSystemComponent.name} } from '${mapping.designSystemComponent.importPath}';`);
21
+ if (mapping.designSystemComponent.dependencies) {
22
+ mapping.designSystemComponent.dependencies.forEach(dep => dependencies.add(dep));
23
+ }
24
+ }
25
+ }
26
+ // 컴포넌트 구조 생성
27
+ componentCode += this.generateReactComponentStructure(componentName, figmaData.document, mappedComponents);
28
+ // 최상단에 임포트 추가
29
+ const importsCode = Array.from(imports).join('\n');
30
+ const dependenciesCode = Array.from(dependencies).length > 0
31
+ ? `\n// Dependencies: ${Array.from(dependencies).join(', ')}`
32
+ : '';
33
+ // GitHub 저장소 정보 추가
34
+ const repositoryInfo = `// Design System Components from GitHub:
35
+ // React: https://github.com/dealicious-inc/ssm-web/tree/master/packages/design-system-react
36
+ // Vue: https://github.com/dealicious-inc/ssm-web/tree/master/packages/design-system
37
+ //
38
+ // To use these components, install the packages:
39
+ // npm install @dealicious/design-system-react @dealicious/design-system
40
+ // or
41
+ // yarn add @dealicious/design-system-react @dealicious/design-system
42
+ `;
43
+ return `${repositoryInfo}\n${importsCode}${dependenciesCode}\n\n${componentCode}`;
44
+ }
45
+ /**
46
+ * HTML을 렌더링하여 이미지로 변환
47
+ */
48
+ async generateImagePreview(htmlContent, componentName, requestId) {
49
+ const browser = await puppeteer.launch({
50
+ headless: true,
51
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
52
+ });
53
+ try {
54
+ const page = await browser.newPage();
55
+ // 뷰포트 설정
56
+ await page.setViewport({
57
+ width: 1200,
58
+ height: 800,
59
+ deviceScaleFactor: 2
60
+ });
61
+ // HTML 콘텐츠 설정
62
+ await page.setContent(htmlContent, {
63
+ waitUntil: 'networkidle0'
64
+ });
65
+ // 컴포넌트가 렌더링될 때까지 대기
66
+ await page.waitForSelector('#root', { timeout: 5000 }).catch(() => {
67
+ // root 요소가 없어도 계속 진행
68
+ });
69
+ // 추가 대기 (렌더링 완료를 위해)
70
+ await page.waitForTimeout(1000);
71
+ // 페이지 높이 계산
72
+ const bodyHeight = await page.evaluate(() => {
73
+ return Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight);
74
+ });
75
+ // 스크린샷 촬영
76
+ const screenshot = await page.screenshot({
77
+ type: 'png',
78
+ fullPage: true,
79
+ clip: {
80
+ x: 0,
81
+ y: 0,
82
+ width: 1200,
83
+ height: Math.min(bodyHeight, 8000) // 최대 8000px
84
+ }
85
+ });
86
+ // 이미지 파일 저장
87
+ const imageFile = await saveBinaryFile(requestId, `${componentName}.png`, screenshot);
88
+ return imageFile;
89
+ }
90
+ finally {
91
+ await browser.close();
92
+ }
93
+ }
94
+ /**
95
+ * React 컴포넌트 생성 및 파일 저장
96
+ */
97
+ async generateAndSaveReactComponent(figmaData, componentName, figmaUrl, nodeId, previewType = 'both') {
98
+ const requestId = generateRequestId();
99
+ const folderPath = getRequestFolderPath(requestId);
100
+ // React 컴포넌트 코드 생성
101
+ const reactCode = await this.generateReactComponent(figmaData, componentName);
102
+ // 컴포넌트 파일 저장
103
+ const componentFile = await saveFile(requestId, `${componentName}.tsx`, reactCode);
104
+ // HTML 미리보기 파일 생성
105
+ const htmlContent = this.generateHTMLPreview(reactCode, componentName);
106
+ let htmlFile;
107
+ let imageFile;
108
+ // 사용자 선택에 따라 파일 생성
109
+ if (previewType === 'html' || previewType === 'both') {
110
+ htmlFile = await saveFile(requestId, `${componentName}.html`, htmlContent);
111
+ }
112
+ if (previewType === 'image' || previewType === 'both') {
113
+ try {
114
+ imageFile = await this.generateImagePreview(htmlContent, componentName, requestId);
115
+ }
116
+ catch (error) {
117
+ console.warn('이미지 생성 실패:', error);
118
+ // 이미지 생성 실패 시에도 계속 진행
119
+ }
120
+ }
121
+ // 메타데이터 저장
122
+ const metadataFile = await saveMetadata(requestId, {
123
+ type: 'react',
124
+ componentName,
125
+ figmaUrl,
126
+ nodeId,
127
+ });
128
+ return {
129
+ requestId,
130
+ folderPath,
131
+ componentFile,
132
+ htmlFile,
133
+ imageFile,
134
+ metadataFile,
135
+ };
136
+ }
137
+ /**
138
+ * Figma 데이터에서 Vue 컴포넌트 생성
139
+ */
140
+ async generateVueComponent(figmaData, componentName) {
141
+ const imports = new Set();
142
+ const dependencies = new Set();
143
+ let componentCode = '';
144
+ // Figma 구조 분석 및 디자인 시스템 컴포넌트 매핑
145
+ const mappedComponents = this.mapFigmaToDesignSystem(figmaData.document, 'vue');
146
+ // Generate imports
147
+ for (const mapping of mappedComponents) {
148
+ if (mapping.designSystemComponent) {
149
+ // Vue는 default import 사용하는 것을 권장
150
+ imports.add(`import ${mapping.designSystemComponent.name} from '${mapping.designSystemComponent.importPath}';`);
151
+ if (mapping.designSystemComponent.dependencies) {
152
+ mapping.designSystemComponent.dependencies.forEach(dep => dependencies.add(dep));
153
+ }
154
+ }
155
+ }
156
+ // 컴포넌트 구조 생성
157
+ componentCode += this.generateVueComponentStructure(componentName, figmaData.document, mappedComponents);
158
+ // 최상단에 임포트 추가
159
+ const importsCode = Array.from(imports).join('\n');
160
+ const dependenciesCode = Array.from(dependencies).length > 0
161
+ ? `\n// Dependencies: ${Array.from(dependencies).join(', ')}`
162
+ : '';
163
+ return `${importsCode}${dependenciesCode}\n\n${componentCode}`;
164
+ }
165
+ /**
166
+ * Vue 컴포넌트 생성 및 파일 저장
167
+ */
168
+ async generateAndSaveVueComponent(figmaData, componentName, figmaUrl, nodeId, previewType = 'both') {
169
+ const requestId = generateRequestId();
170
+ const folderPath = getRequestFolderPath(requestId);
171
+ // Vue 컴포넌트 코드 생성
172
+ const vueCode = await this.generateVueComponent(figmaData, componentName);
173
+ // 컴포넌트 파일 저장
174
+ const componentFile = await saveFile(requestId, `${componentName}.vue`, vueCode);
175
+ // HTML 미리보기 파일 생성
176
+ const htmlContent = this.generateHTMLPreview(vueCode, componentName, 'vue');
177
+ let htmlFile;
178
+ let imageFile;
179
+ // 사용자 선택에 따라 파일 생성
180
+ if (previewType === 'html' || previewType === 'both') {
181
+ htmlFile = await saveFile(requestId, `${componentName}.html`, htmlContent);
182
+ }
183
+ if (previewType === 'image' || previewType === 'both') {
184
+ try {
185
+ imageFile = await this.generateImagePreview(htmlContent, componentName, requestId);
186
+ }
187
+ catch (error) {
188
+ console.warn('이미지 생성 실패:', error);
189
+ // 이미지 생성 실패 시에도 계속 진행
190
+ }
191
+ }
192
+ // 메타데이터 저장
193
+ const metadataFile = await saveMetadata(requestId, {
194
+ type: 'vue',
195
+ componentName,
196
+ figmaUrl,
197
+ nodeId,
198
+ });
199
+ return {
200
+ requestId,
201
+ folderPath,
202
+ componentFile,
203
+ htmlFile,
204
+ imageFile,
205
+ metadataFile,
206
+ };
207
+ }
208
+ /**
209
+ * HTML 미리보기 파일 생성
210
+ */
211
+ generateHTMLPreview(componentCode, componentName, framework = 'react') {
212
+ const escapedCode = this.escapeHtml(componentCode);
213
+ return `<!DOCTYPE html>
214
+ <html lang="ko">
215
+ <head>
216
+ <meta charset="UTF-8">
217
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
218
+ <title>${componentName} - Preview</title>
219
+ <script crossorigin src="https://unpkg.com/react@19/umd/react.production.min.js"></script>
220
+ <script crossorigin src="https://unpkg.com/react-dom@19/umd/react-dom.production.min.js"></script>
221
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
222
+ <style>
223
+ body {
224
+ margin: 0;
225
+ padding: 20px;
226
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
227
+ background-color: #f5f5f5;
228
+ }
229
+ .container {
230
+ max-width: 1200px;
231
+ margin: 0 auto;
232
+ background: white;
233
+ padding: 20px;
234
+ border-radius: 8px;
235
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
236
+ }
237
+ .preview {
238
+ margin-top: 30px;
239
+ padding: 20px;
240
+ border: 2px dashed #dee2e6;
241
+ border-radius: 4px;
242
+ background: #fff;
243
+ min-height: 200px;
244
+ }
245
+ .error {
246
+ color: red;
247
+ padding: 10px;
248
+ background: #ffe6e6;
249
+ border: 1px solid #ff9999;
250
+ border-radius: 4px;
251
+ margin: 10px 0;
252
+ }
253
+ .mock-button {
254
+ display: inline-block;
255
+ padding: 8px 16px;
256
+ margin: 4px;
257
+ background: #007bff;
258
+ color: white;
259
+ border: none;
260
+ border-radius: 4px;
261
+ cursor: pointer;
262
+ }
263
+ .mock-button:hover {
264
+ background: #0056b3;
265
+ }
266
+ </style>
267
+ </head>
268
+ <body>
269
+ <div class="container">
270
+ <h1>${componentName} - ${framework === 'react' ? 'React' : 'Vue'} 컴포넌트 미리보기</h1>
271
+ <div class="preview">
272
+ <div id="root"></div>
273
+ </div>
274
+ </div>
275
+ <script type="text/babel">
276
+ // 모의 Design System 컴포넌트 정의
277
+ const Button = ({ children, size, ...props }) => {
278
+ const sizeStyle = size === 'small' ? { padding: '4px 8px', fontSize: '12px' } :
279
+ size === 'large' ? { padding: '12px 24px', fontSize: '16px' } :
280
+ { padding: '8px 16px', fontSize: '14px' };
281
+ return React.createElement('button', {
282
+ style: { ...sizeStyle, background: '#007bff', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', margin: '2px' },
283
+ ...props
284
+ }, children);
285
+ };
286
+
287
+ const LayerPopup = ({ children, isOpen, onClose, ...props }) => {
288
+ if (!isOpen) return null;
289
+ return React.createElement('div', {
290
+ style: { position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.5)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' },
291
+ onClick: onClose,
292
+ ...props
293
+ }, React.createElement('div', {
294
+ style: { background: 'white', padding: '20px', borderRadius: '8px', maxWidth: '90%', maxHeight: '90%', overflow: 'auto' },
295
+ onClick: (e) => e.stopPropagation()
296
+ }, children));
297
+ };
298
+
299
+ const Tab = ({ children, tabs, activeTab, onTabChange, ...props }) => {
300
+ return React.createElement('div', { style: { borderBottom: '1px solid #ddd' }, ...props }, children);
301
+ };
302
+
303
+ const Text = ({ children, variant, size, ...props }) => {
304
+ const style = {
305
+ fontSize: size === 'small' ? '12px' : size === 'large' ? '18px' : '14px',
306
+ fontWeight: variant === 'heading' ? 'bold' : 'normal',
307
+ ...props.style
308
+ };
309
+ return React.createElement('span', { style, ...props }, children);
310
+ };
311
+
312
+ const LabeledText = ({ label, value, ...props }) => {
313
+ return React.createElement('div', { style: { margin: '4px 0' }, ...props },
314
+ React.createElement('span', { style: { fontWeight: 'bold', marginRight: '8px' } }, label + ':'),
315
+ React.createElement('span', {}, value || '')
316
+ );
317
+ };
318
+
319
+ const Input = ({ placeholder, value, onChange, ...props }) => {
320
+ return React.createElement('input', {
321
+ type: 'text',
322
+ placeholder,
323
+ value: value || '',
324
+ onChange: onChange || (() => {}),
325
+ style: { padding: '8px', border: '1px solid #ddd', borderRadius: '4px', margin: '4px', width: '200px' },
326
+ ...props
327
+ });
328
+ };
329
+
330
+ const Chip = ({ children, variant, size, onDelete, ...props }) => {
331
+ return React.createElement('span', {
332
+ style: {
333
+ display: 'inline-block',
334
+ padding: '4px 8px',
335
+ background: variant === 'filled' ? '#007bff' : '#f0f0f0',
336
+ color: variant === 'filled' ? 'white' : 'black',
337
+ borderRadius: '16px',
338
+ margin: '2px',
339
+ fontSize: size === 'small' ? '12px' : '14px'
340
+ },
341
+ ...props
342
+ }, children, onDelete && React.createElement('button', {
343
+ onClick: onDelete,
344
+ style: { marginLeft: '8px', background: 'none', border: 'none', cursor: 'pointer' }
345
+ }, '×'));
346
+ };
347
+
348
+ const Switch = ({ checked, onChange, label, ...props }) => {
349
+ return React.createElement('label', { style: { display: 'flex', alignItems: 'center', margin: '4px' }, ...props },
350
+ React.createElement('input', {
351
+ type: 'checkbox',
352
+ checked: checked || false,
353
+ onChange: onChange || (() => {}),
354
+ style: { marginRight: '8px' }
355
+ }),
356
+ label && React.createElement('span', {}, label)
357
+ );
358
+ };
359
+
360
+ const ArrowPagination = ({ currentPage, totalPages, onPageChange, children, ...props }) => {
361
+ return React.createElement('div', { style: { display: 'flex', alignItems: 'center', margin: '4px' }, ...props }, children);
362
+ };
363
+
364
+ const Accordion = ({ title, children, defaultExpanded, disabled, ...props }) => {
365
+ const [expanded, setExpanded] = React.useState(defaultExpanded || false);
366
+ if (disabled) return null;
367
+ return React.createElement('div', { style: { border: '1px solid #ddd', borderRadius: '4px', margin: '4px' }, ...props },
368
+ React.createElement('div', {
369
+ onClick: () => setExpanded(!expanded),
370
+ style: { padding: '8px', cursor: 'pointer', background: '#f5f5f5', fontWeight: 'bold' }
371
+ }, title || 'Accordion'),
372
+ expanded && React.createElement('div', { style: { padding: '8px' } }, children)
373
+ );
374
+ };
375
+
376
+ // 컴포넌트 코드 정리
377
+ const cleanCode = ${JSON.stringify(componentCode)}
378
+ .replace(/import[^;]+;/g, '')
379
+ .replace(/\/\/[^\\n]*/g, '')
380
+ .replace(/interface[^\\{]*\\{[^\\}]*\\}/g, '')
381
+ .replace(/React\\.FC<[^>]*>/g, '')
382
+ .replace(/export default ${componentName};/g, '')
383
+ .replace(new RegExp(\`const ${componentName}:.*?=\`, 'g'), \`const ${componentName} = \`)
384
+ // 문법 오류 수정: 잘못된 prop 형식 수정
385
+ .replace(/<Button\\s+>([^<]+)\\s+size="([^"]+)">/g, '<Button size="$2">$1</Button>')
386
+ .replace(/<Button\\s+>([^<]+)\\s+size="([^"]+)">/g, '<Button size="$2">$1</Button>')
387
+ // 빈 Button 태그 정리
388
+ .replace(/<Button\\s*><\\/Button>/g, '<Button></Button>')
389
+ .replace(/<Button\\s+size="([^"]+)"\\s*><\\/Button>/g, '<Button size="$1"></Button>');
390
+
391
+ try {
392
+ const transformedCode = Babel.transform(cleanCode, { presets: ['react'] }).code;
393
+ eval(transformedCode);
394
+ const root = ReactDOM.createRoot(document.getElementById('root'));
395
+ root.render(React.createElement(${componentName}, {}));
396
+ } catch (error) {
397
+ console.error('렌더링 오류:', error);
398
+ const errorDiv = document.getElementById('root');
399
+ errorDiv.innerHTML = '<div class="error"><strong>렌더링 오류:</strong><br>' +
400
+ error.message + '<br><br><strong>스택:</strong><br>' +
401
+ (error.stack || '스택 정보 없음') + '</div>';
402
+ }
403
+ </script>
404
+ </body>
405
+ </html>`;
406
+ }
407
+ /**
408
+ * HTML 이스케이프
409
+ */
410
+ escapeHtml(text) {
411
+ const map = {
412
+ '&': '&amp;',
413
+ '<': '&lt;',
414
+ '>': '&gt;',
415
+ '"': '&quot;',
416
+ "'": '&#039;'
417
+ };
418
+ return text.replace(/[&<>"']/g, (m) => map[m]);
419
+ }
420
+ /**
421
+ * Figma 노드를 디자인 시스템 컴포넌트에 매핑
422
+ */
423
+ mapFigmaToDesignSystem(node, framework) {
424
+ const mappings = [];
425
+ // 현재 노드 분석
426
+ const mapping = this.analyzeNode(node, framework);
427
+ mappings.push(mapping);
428
+ // 자식 노드 재귀적 분석
429
+ if (node.children) {
430
+ for (const child of node.children) {
431
+ mappings.push(...this.mapFigmaToDesignSystem(child, framework));
432
+ }
433
+ }
434
+ return mappings;
435
+ }
436
+ /**
437
+ * 단일 Figma 노드 분석 및 디자인 시스템 최적 매칭 찾기
438
+ * 항상 디자인 시스템 컴포넌트를 반환 - null이 아님
439
+ */
440
+ analyzeNode(node, framework) {
441
+ // 보이지 않는 노드 건너뛰기
442
+ if (node.visible === false) {
443
+ const defaultComponent = this.designSystemService.getComponent('Button', framework);
444
+ if (!defaultComponent) {
445
+ // Button은 항상 사용 가능하므로 이 경우는 절대 발생하지 않음
446
+ throw new Error('No Design System components available');
447
+ }
448
+ return {
449
+ figmaNode: node,
450
+ designSystemComponent: defaultComponent,
451
+ confidence: 0.1,
452
+ };
453
+ }
454
+ // 노드 속성에 따라 매칭 컴포넌트 찾기
455
+ let component = this.findComponentByNodeProperties(node, framework);
456
+ // 매칭 컴포넌트가 없으면 노드 유형에 따라 기본 컴포넌트 사용
457
+ if (!component) {
458
+ component = this.getDefaultComponentByType(node, framework);
459
+ }
460
+ // component가 null인 경우 (컨테이너 역할만 하는 경우) div로 처리하기 위해 특별한 컴포넌트 사용
461
+ // 실제로는 generateReactJSX에서 null 체크하여 div로 처리
462
+ if (!component) {
463
+ // div로 처리하기 위한 플레이스홀더 컴포넌트 생성
464
+ component = {
465
+ name: 'div',
466
+ description: 'Container div',
467
+ category: 'Layout',
468
+ importPath: '',
469
+ props: [],
470
+ examples: [],
471
+ };
472
+ }
473
+ return {
474
+ figmaNode: node,
475
+ designSystemComponent: component,
476
+ confidence: component.name === 'div' ? 0.5 : 0.8,
477
+ };
478
+ }
479
+ /**
480
+ * Figma 노드 속성에 따라 디자인 시스템 컴포넌트 찾기
481
+ * 노드 타입, 이름, 구조, 속성을 종합적으로 분석하여 적절한 컴포넌트 매핑
482
+ */
483
+ findComponentByNodeProperties(node, framework) {
484
+ const nodeName = node.name.toLowerCase();
485
+ const nodeType = node.type;
486
+ // 1. 직접 이름 매칭 (가장 정확한 매칭)
487
+ let component = this.designSystemService.findBestMatch(nodeName, framework);
488
+ if (component)
489
+ return component;
490
+ // 2. 노드 타입과 구조 기반 매칭
491
+ switch (nodeType) {
492
+ case 'TEXT':
493
+ // TEXT 노드는 Text 컴포넌트 사용
494
+ // 단, 버튼 레이블로 사용되는 경우는 제외
495
+ if (this.isButtonLabel(node)) {
496
+ return this.designSystemService.getComponent('Button', framework);
497
+ }
498
+ // 링크 텍스트인지 확인
499
+ if (nodeName.includes('link') || nodeName.includes('href') || this.hasLinkParent(node)) {
500
+ return this.designSystemService.getComponent('TextLink', framework);
501
+ }
502
+ // 기본 텍스트는 Text 컴포넌트 사용
503
+ return this.designSystemService.getComponent('Text', framework);
504
+ case 'FRAME':
505
+ // FRAME 노드는 자식 구조를 분석하여 적절한 컴포넌트 선택
506
+ return this.analyzeFrameNode(node, framework);
507
+ case 'COMPONENT':
508
+ // Component 인스턴스 - 이름 기반 매칭
509
+ component = this.designSystemService.findBestMatch(nodeName, framework);
510
+ if (component)
511
+ return component;
512
+ // 컴포넌트 인스턴스는 자식 구조 분석
513
+ return this.analyzeFrameNode(node, framework);
514
+ case 'RECTANGLE':
515
+ // RECTANGLE 노드는 이름과 자식 구조 분석
516
+ if (nodeName.includes('button') || nodeName.includes('btn') || nodeName.includes('click')) {
517
+ return this.designSystemService.getComponent('Button', framework);
518
+ }
519
+ if (nodeName.includes('input') || nodeName.includes('field')) {
520
+ return this.designSystemService.getComponent('Input', framework);
521
+ }
522
+ if (nodeName.includes('badge') || nodeName.includes('tag') || nodeName.includes('label')) {
523
+ const badgeComponent = this.designSystemService.getComponent('Badge', framework);
524
+ if (badgeComponent)
525
+ return badgeComponent;
526
+ return this.designSystemService.getComponent('Tag', framework);
527
+ }
528
+ if (nodeName.includes('chip')) {
529
+ return this.designSystemService.getComponent('Chip', framework);
530
+ }
531
+ // 사각형은 자식 구조 분석
532
+ return this.analyzeFrameNode(node, framework);
533
+ case 'GROUP':
534
+ // GROUP은 컨테이너 역할만 하므로 자식 구조 분석
535
+ return this.analyzeFrameNode(node, framework);
536
+ default:
537
+ // 알 수 없는 타입은 자식 구조 분석
538
+ return this.analyzeFrameNode(node, framework);
539
+ }
540
+ }
541
+ /**
542
+ * FRAME 노드 분석 - 자식 구조를 기반으로 적절한 컴포넌트 선택
543
+ */
544
+ analyzeFrameNode(node, framework) {
545
+ const nodeName = node.name.toLowerCase();
546
+ // 이름 기반 매칭 (우선순위 높음)
547
+ if (nodeName.includes('tab') || nodeName.includes('tabs')) {
548
+ return this.designSystemService.getComponent('Tab', framework);
549
+ }
550
+ if (nodeName.includes('accordion') || nodeName.includes('collapse')) {
551
+ return this.designSystemService.getComponent('Accordion', framework);
552
+ }
553
+ if (nodeName.includes('modal') || nodeName.includes('dialog') || nodeName.includes('popup')) {
554
+ return this.designSystemService.getComponent('Modal', framework);
555
+ }
556
+ if (nodeName.includes('button') || nodeName.includes('btn')) {
557
+ return this.designSystemService.getComponent('Button', framework);
558
+ }
559
+ if (nodeName.includes('badge')) {
560
+ return this.designSystemService.getComponent('Badge', framework);
561
+ }
562
+ if (nodeName.includes('tag')) {
563
+ return this.designSystemService.getComponent('Tag', framework);
564
+ }
565
+ if (nodeName.includes('chip')) {
566
+ return this.designSystemService.getComponent('Chip', framework);
567
+ }
568
+ if (nodeName.includes('labeled') || nodeName.includes('label-text')) {
569
+ return this.designSystemService.getComponent('LabeledText', framework);
570
+ }
571
+ if (nodeName.includes('text-link') || nodeName.includes('link')) {
572
+ return this.designSystemService.getComponent('TextLink', framework);
573
+ }
574
+ // 자식 노드 구조 분석
575
+ if (node.children && node.children.length > 0) {
576
+ // 자식이 모두 TEXT인 경우 - Text 또는 LabeledText
577
+ const allTextChildren = node.children.every(child => child.type === 'TEXT');
578
+ if (allTextChildren && node.children.length === 1) {
579
+ return this.designSystemService.getComponent('Text', framework);
580
+ }
581
+ if (allTextChildren && node.children.length === 2) {
582
+ // 라벨과 값이 있는 경우 LabeledText
583
+ return this.designSystemService.getComponent('LabeledText', framework);
584
+ }
585
+ // 자식에 Tab이 있는 경우 - Tab 컴포넌트
586
+ const hasTabChildren = node.children.some(child => child.name.toLowerCase().includes('tab') ||
587
+ child.type === 'COMPONENT' && child.name.toLowerCase().includes('tab'));
588
+ if (hasTabChildren) {
589
+ return this.designSystemService.getComponent('Tab', framework);
590
+ }
591
+ // 자식에 Accordion이 있는 경우
592
+ const hasAccordionChildren = node.children.some(child => child.name.toLowerCase().includes('accordion') ||
593
+ child.type === 'COMPONENT' && child.name.toLowerCase().includes('accordion'));
594
+ if (hasAccordionChildren) {
595
+ return this.designSystemService.getComponent('Accordion', framework);
596
+ }
597
+ // 레이아웃 모드가 있는 경우 (Auto Layout)
598
+ if (node.layoutMode === 'HORIZONTAL' || node.layoutMode === 'VERTICAL') {
599
+ // Auto Layout은 일반적으로 컨테이너 역할
600
+ // 자식이 버튼인 경우 Button 그룹으로 처리하지 않고 div로 처리
601
+ return null; // null을 반환하면 div로 처리
602
+ }
603
+ }
604
+ // 기본적으로 컨테이너 역할만 하는 경우 null 반환 (div로 처리)
605
+ return null;
606
+ }
607
+ /**
608
+ * 노드가 버튼 레이블인지 확인
609
+ */
610
+ isButtonLabel(node) {
611
+ // 부모 노드가 버튼인지 확인하는 로직은 현재 노드 구조로는 어려움
612
+ // 이름 기반으로 판단
613
+ const nodeName = node.name.toLowerCase();
614
+ return nodeName.includes('button') || nodeName.includes('btn') || nodeName.includes('click');
615
+ }
616
+ /**
617
+ * 노드가 링크의 일부인지 확인
618
+ */
619
+ hasLinkParent(node) {
620
+ // 현재 구현에서는 이름 기반으로 판단
621
+ const nodeName = node.name.toLowerCase();
622
+ return nodeName.includes('link') || nodeName.includes('href');
623
+ }
624
+ /**
625
+ * 기본 디자인 시스템 컴포넌트 가져오기
626
+ */
627
+ async getDefaultComponent(framework) {
628
+ // Button을 기본 컴포넌트로 사용 (항상 존재함)
629
+ const buttonComponent = this.designSystemService.getComponent('Button', framework);
630
+ if (buttonComponent)
631
+ return buttonComponent;
632
+ // Button이 없으면 첫 번째 사용 가능한 컴포넌트 사용
633
+ const components = await this.designSystemService.getAvailableComponents(framework);
634
+ if (components.length === 0) {
635
+ throw new Error('No Design System components available');
636
+ }
637
+ return components[0];
638
+ }
639
+ /**
640
+ * 노드 유형에 따라 기본 컴포넌트 가져오기
641
+ * findComponentByNodeProperties에서 매칭되지 않은 경우에만 사용
642
+ */
643
+ getDefaultComponentByType(node, framework) {
644
+ const nodeType = node.type;
645
+ const nodeName = node.name.toLowerCase();
646
+ // 노드 유형과 이름에 따라 스마트 기본값
647
+ switch (nodeType) {
648
+ case 'TEXT':
649
+ // Text 노드는 Text 컴포넌트 사용
650
+ const textComponent = this.designSystemService.getComponent('Text', framework);
651
+ if (textComponent)
652
+ return textComponent;
653
+ break;
654
+ case 'RECTANGLE':
655
+ // Rectangle은 이름 기반으로 판단
656
+ if (nodeName.includes('button') || nodeName.includes('btn')) {
657
+ const buttonComponent = this.designSystemService.getComponent('Button', framework);
658
+ if (buttonComponent)
659
+ return buttonComponent;
660
+ }
661
+ if (nodeName.includes('badge') || nodeName.includes('tag')) {
662
+ const badgeComponent = this.designSystemService.getComponent('Badge', framework);
663
+ if (badgeComponent)
664
+ return badgeComponent;
665
+ }
666
+ // 기본적으로 div로 처리 (null 반환하면 div 사용)
667
+ break;
668
+ case 'FRAME':
669
+ case 'GROUP':
670
+ // Frame과 Group은 컨테이너 역할만 하므로 div 사용 (null 반환)
671
+ break;
672
+ default:
673
+ // 알 수 없는 타입은 div 사용
674
+ break;
675
+ }
676
+ // 최종 폴백 - Button 사용 (최후의 수단)
677
+ const finalButtonComponent = this.designSystemService.getComponent('Button', framework);
678
+ if (finalButtonComponent)
679
+ return finalButtonComponent;
680
+ // Button이 없으면 첫 번째 사용 가능한 컴포넌트 사용
681
+ const components = framework === 'react'
682
+ ? this.designSystemService['reactComponents']
683
+ : this.designSystemService['vueComponents'];
684
+ if (components && components.length > 0) {
685
+ return components[0];
686
+ }
687
+ throw new Error('No Design System components available');
688
+ }
689
+ /**
690
+ * React 컴포넌트 구조 생성
691
+ */
692
+ generateReactComponentStructure(componentName, rootNode, mappedComponents) {
693
+ let code = `import React from 'react';\n\n`;
694
+ code += `interface ${componentName}Props {\n`;
695
+ code += ` // Add your props here\n`;
696
+ code += `}\n\n`;
697
+ code += `const ${componentName}: React.FC<${componentName}Props> = (props) => {\n`;
698
+ code += ` return (\n`;
699
+ code += ` <div>\n`;
700
+ // 매핑된 컴포넌트에 따라 JSX 생성
701
+ code += this.generateReactJSX(rootNode, mappedComponents, 2);
702
+ code += ` </div>\n`;
703
+ code += ` );\n`;
704
+ code += `};\n\n`;
705
+ code += `export default ${componentName};\n`;
706
+ return code;
707
+ }
708
+ /**
709
+ * Vue 컴포넌트 구조 생성
710
+ */
711
+ generateVueComponentStructure(componentName, rootNode, mappedComponents) {
712
+ let code = `<template>\n`;
713
+ code += ` <div>\n`;
714
+ // 매핑된 컴포넌트에 따라 템플릿 생성
715
+ code += this.generateVueTemplate(rootNode, mappedComponents, 2);
716
+ code += ` </div>\n`;
717
+ code += `</template>\n\n`;
718
+ code += `<script setup lang="ts">\n`;
719
+ code += `// Add your reactive data and methods here\n`;
720
+ code += `</script>\n\n`;
721
+ code += `<style scoped>\n`;
722
+ code += `/* Add your styles here */\n`;
723
+ code += `</style>\n`;
724
+ return code;
725
+ }
726
+ /**
727
+ * Figma 노드에서 React JSX 생성
728
+ * 불필요한 중첩을 제거하여 깊이를 최소화
729
+ */
730
+ generateReactJSX(node, mappedComponents, indent = 0, parentComponent) {
731
+ const indentStr = ' '.repeat(indent);
732
+ let jsx = '';
733
+ // 보이지 않는 노드는 건너뛰기
734
+ if (node.visible === false) {
735
+ return '';
736
+ }
737
+ // 현재 노드에 대한 매핑 찾기
738
+ const mapping = mappedComponents.find(m => m.figmaNode.id === node.id);
739
+ if (mapping?.designSystemComponent) {
740
+ const component = mapping.designSystemComponent;
741
+ // 1. 같은 타입의 컴포넌트가 중첩되는 경우 부모를 건너뛰고 자식만 렌더링
742
+ if (parentComponent && parentComponent.name === component.name) {
743
+ // 같은 타입 중첩 방지 - 자식만 렌더링
744
+ if (node.children) {
745
+ for (const child of node.children) {
746
+ jsx += this.generateReactJSX(child, mappedComponents, indent, component);
747
+ }
748
+ }
749
+ else if (node.characters) {
750
+ // 텍스트만 있는 경우 직접 반환
751
+ return `${indentStr}${node.characters}\n`;
752
+ }
753
+ return jsx;
754
+ }
755
+ // 2. div 컨테이너는 가능한 한 건너뛰기 (자식이 하나만 있는 경우)
756
+ if (component.name === 'div') {
757
+ // 빈 div 제거
758
+ if (!node.children && !node.characters) {
759
+ return '';
760
+ }
761
+ // 자식이 하나만 있고 그 자식도 div가 아닌 경우 div를 건너뛰기
762
+ if (node.children && node.children.length === 1) {
763
+ const singleChild = node.children[0];
764
+ const childMapping = mappedComponents.find(m => m.figmaNode.id === singleChild.id);
765
+ if (childMapping && childMapping.designSystemComponent.name !== 'div') {
766
+ // div를 건너뛰고 자식만 렌더링
767
+ return this.generateReactJSX(singleChild, mappedComponents, indent, component);
768
+ }
769
+ }
770
+ // 여러 자식이 있거나 자식이 div인 경우에만 div 사용
771
+ // className은 추가하지 않음 (불필요한 className 방지)
772
+ const style = this.generateInlineStyle(node);
773
+ jsx += `${indentStr}<div${style}>\n`;
774
+ if (node.children) {
775
+ for (const child of node.children) {
776
+ jsx += this.generateReactJSX(child, mappedComponents, indent + 1, component);
777
+ }
778
+ }
779
+ else if (node.characters) {
780
+ jsx += `${indentStr} ${node.characters}\n`;
781
+ }
782
+ jsx += `${indentStr}</div>\n`;
783
+ return jsx;
784
+ }
785
+ // 3. 빈 컴포넌트 제거
786
+ const hasChildren = node.children && node.children.length > 0;
787
+ const hasText = node.characters && node.characters.trim().length > 0;
788
+ if (!hasChildren && !hasText) {
789
+ if (['Button', 'Chip', 'Badge', 'Tag', 'Icon'].includes(component.name)) {
790
+ return '';
791
+ }
792
+ if (component.name === 'Text' || component.name === 'TextLink') {
793
+ return '';
794
+ }
795
+ }
796
+ // 4. 자식이 하나만 있고 같은 타입이 아닌 경우 중간 노드 건너뛰기 고려
797
+ // 단, Chip, Badge, Tag는 텍스트를 children으로 받으므로 예외
798
+ if (hasChildren && node.children && node.children.length === 1 && !hasText) {
799
+ const singleChild = node.children[0];
800
+ const childMapping = mappedComponents.find(m => m.figmaNode.id === singleChild.id);
801
+ // Chip, Badge, Tag는 텍스트를 children으로 받으므로 중간 노드 건너뛰지 않음
802
+ if (['Chip', 'Badge', 'Tag'].includes(component.name)) {
803
+ // Chip/Badge/Tag 안에 Text가 있으면 Text를 children으로 사용
804
+ if (childMapping && childMapping.designSystemComponent.name === 'Text' && singleChild.characters) {
805
+ // Text 노드의 텍스트를 직접 children으로 사용
806
+ const props = this.generateComponentProps(node, component);
807
+ jsx += `${indentStr}<${component.name}${props}>\n`;
808
+ jsx += `${indentStr} ${singleChild.characters}\n`;
809
+ jsx += `${indentStr}</${component.name}>\n`;
810
+ return jsx;
811
+ }
812
+ }
813
+ // 자식이 같은 타입이 아니고, 자식이 텍스트나 의미있는 컴포넌트인 경우
814
+ if (childMapping && childMapping.designSystemComponent.name !== component.name) {
815
+ // 부모 컴포넌트의 props를 자식에 병합할 수 있는지 확인
816
+ // 단순 컨테이너 역할만 하는 경우 자식만 렌더링
817
+ if (this.isSimpleContainer(component, node)) {
818
+ return this.generateReactJSX(singleChild, mappedComponents, indent, component);
819
+ }
820
+ }
821
+ }
822
+ // 5. 디자인 시스템 컴포넌트 렌더링
823
+ const props = this.generateComponentProps(node, component);
824
+ // Chip, Badge, Tag는 자식이 Text인 경우 텍스트만 추출
825
+ // 또는 자식이 Button이고 그 안에 Text나 Tag가 있는 경우 Button을 제거
826
+ if (['Chip', 'Badge', 'Tag'].includes(component.name) && hasChildren && !hasText && node.children) {
827
+ // 자식이 모두 Button이고 그 안에 Text나 Tag가 있는 경우 Button 제거
828
+ const buttonChildren = node.children.filter(child => {
829
+ const childMapping = mappedComponents.find(m => m.figmaNode.id === child.id);
830
+ return childMapping && childMapping.designSystemComponent.name === 'Button';
831
+ });
832
+ if (buttonChildren.length === node.children.length && buttonChildren.length > 0) {
833
+ // Button의 자식을 직접 사용
834
+ const directChildren = [];
835
+ for (const buttonChild of buttonChildren) {
836
+ if (buttonChild.children) {
837
+ directChildren.push(...buttonChild.children);
838
+ }
839
+ }
840
+ // 직접 자식이 모두 Text나 Tag인 경우
841
+ if (directChildren.length > 0) {
842
+ const textOrTagChildren = directChildren.filter(child => {
843
+ const childMapping = mappedComponents.find(m => m.figmaNode.id === child.id);
844
+ return childMapping &&
845
+ (childMapping.designSystemComponent.name === 'Text' ||
846
+ childMapping.designSystemComponent.name === 'Tag' ||
847
+ child.characters);
848
+ });
849
+ // Button을 제거하고 직접 자식만 사용
850
+ if (textOrTagChildren.length > 0) {
851
+ jsx += `${indentStr}<${component.name}${props}>\n`;
852
+ for (const child of directChildren) {
853
+ jsx += this.generateReactJSX(child, mappedComponents, indent + 1, component);
854
+ }
855
+ jsx += `${indentStr}</${component.name}>\n`;
856
+ return jsx;
857
+ }
858
+ }
859
+ }
860
+ // 자식이 모두 Text인 경우 텍스트만 children으로 사용
861
+ const textChildren = node.children ? node.children.filter((child) => {
862
+ const childMapping = mappedComponents.find(m => m.figmaNode.id === child.id);
863
+ return childMapping &&
864
+ (childMapping.designSystemComponent.name === 'Text' ||
865
+ child.characters);
866
+ }) : [];
867
+ if (node.children && textChildren.length === node.children.length && textChildren.length > 0) {
868
+ const textContent = textChildren
869
+ .map((child) => child.characters || '')
870
+ .filter((text) => text.trim().length > 0)
871
+ .join(' ');
872
+ if (textContent) {
873
+ jsx += `${indentStr}<${component.name}${props}>\n`;
874
+ jsx += `${indentStr} ${textContent}\n`;
875
+ jsx += `${indentStr}</${component.name}>\n`;
876
+ return jsx;
877
+ }
878
+ }
879
+ }
880
+ jsx += `${indentStr}<${component.name}${props}>\n`;
881
+ if (hasChildren && node.children) {
882
+ for (const child of node.children) {
883
+ jsx += this.generateReactJSX(child, mappedComponents, indent + 1, component);
884
+ }
885
+ }
886
+ else if (hasText) {
887
+ jsx += `${indentStr} ${node.characters}\n`;
888
+ }
889
+ jsx += `${indentStr}</${component.name}>\n`;
890
+ }
891
+ else {
892
+ // 매핑이 없는 경우 div로 처리
893
+ if (!node.children && !node.characters) {
894
+ return '';
895
+ }
896
+ // 자식이 하나만 있는 경우 div를 건너뛰기
897
+ if (node.children && node.children.length === 1 && !node.characters) {
898
+ return this.generateReactJSX(node.children[0], mappedComponents, indent, parentComponent);
899
+ }
900
+ // className은 추가하지 않음 (불필요한 className 방지)
901
+ const style = this.generateInlineStyle(node);
902
+ jsx += `${indentStr}<div${style}>\n`;
903
+ if (node.children) {
904
+ for (const child of node.children) {
905
+ jsx += this.generateReactJSX(child, mappedComponents, indent + 1, parentComponent);
906
+ }
907
+ }
908
+ else if (node.characters) {
909
+ jsx += `${indentStr} ${node.characters}\n`;
910
+ }
911
+ jsx += `${indentStr}</div>\n`;
912
+ }
913
+ return jsx;
914
+ }
915
+ /**
916
+ * 컴포넌트가 단순 컨테이너 역할만 하는지 확인
917
+ */
918
+ isSimpleContainer(component, node) {
919
+ // div는 항상 컨테이너
920
+ if (component.name === 'div')
921
+ return true;
922
+ // Button이 텍스트나 의미있는 자식 없이 자식만 있는 경우 컨테이너로 간주
923
+ if (component.name === 'Button' && !node.characters && node.children) {
924
+ // 자식이 모두 빈 노드가 아닌 경우에만 컨테이너로 간주
925
+ return true;
926
+ }
927
+ // Chip, Badge, Tag도 비슷하게 처리
928
+ if (['Chip', 'Badge', 'Tag'].includes(component.name) && !node.characters && node.children) {
929
+ return true;
930
+ }
931
+ return false;
932
+ }
933
+ /**
934
+ * Figma 노드에서 Vue 템플릿 생성
935
+ */
936
+ generateVueTemplate(node, mappedComponents, indent = 0) {
937
+ const indentStr = ' '.repeat(indent);
938
+ let template = '';
939
+ // 현재 노드에 대한 매핑 찾기
940
+ const mapping = mappedComponents.find(m => m.figmaNode.id === node.id);
941
+ // 항상 디자인 시스템 컴포넌트 사용 - HTML로 폴백하지 않음
942
+ if (mapping?.designSystemComponent) {
943
+ const component = mapping.designSystemComponent;
944
+ const props = this.generateComponentProps(node, component);
945
+ template += `${indentStr}<${component.name}${props}>\n`;
946
+ if (node.children) {
947
+ for (const child of node.children) {
948
+ template += this.generateVueTemplate(child, mappedComponents, indent + 1);
949
+ }
950
+ }
951
+ else if (node.characters) {
952
+ template += `${indentStr} ${node.characters}\n`;
953
+ }
954
+ template += `${indentStr}</${component.name}>\n`;
955
+ }
956
+ else {
957
+ // 일반적인 패턴 기반 유사 매칭을 사용하므로 이 경우는 절대 발생하지 않음
958
+ console.warn(`No Design System component found for node ${node.id}, using Button as fallback`);
959
+ const fallbackComponent = this.designSystemService.getComponent('Button', 'vue');
960
+ if (fallbackComponent) {
961
+ template += `${indentStr}<${fallbackComponent.name}>\n`;
962
+ if (node.children) {
963
+ for (const child of node.children) {
964
+ template += this.generateVueTemplate(child, mappedComponents, indent + 1);
965
+ }
966
+ }
967
+ else if (node.characters) {
968
+ template += `${indentStr} ${node.characters}\n`;
969
+ }
970
+ template += `${indentStr}</${fallbackComponent.name}>\n`;
971
+ }
972
+ }
973
+ return template;
974
+ }
975
+ /**
976
+ * Figma 노드와 디자인 시스템 컴포넌트에 따라 컴포넌트 속성 생성
977
+ */
978
+ generateComponentProps(node, component) {
979
+ const props = [];
980
+ // Input 컴포넌트의 경우 placeholder 추가
981
+ if (node.characters && component.name === 'Input') {
982
+ props.push(`placeholder="${node.characters}"`);
983
+ }
984
+ // Text 컴포넌트의 경우 children으로 텍스트 전달 (props가 아닌)
985
+ // Text 컴포넌트는 children으로 텍스트를 받으므로 props에 추가하지 않음
986
+ // 바운딩 박스에 따라 크기 속성 추가
987
+ if (node.absoluteBoundingBox) {
988
+ const { width, height } = node.absoluteBoundingBox;
989
+ if (component.name === 'Button') {
990
+ if (height < 32)
991
+ props.push('size="small"');
992
+ else if (height > 48)
993
+ props.push('size="large"');
994
+ else if (height >= 32)
995
+ props.push('size="medium"');
996
+ }
997
+ // Chip, Badge, Tag도 크기 속성 지원하는 경우
998
+ if (component.name === 'Chip' || component.name === 'Badge' || component.name === 'Tag') {
999
+ if (height < 24)
1000
+ props.push('size="small"');
1001
+ else if (height > 32)
1002
+ props.push('size="large"');
1003
+ }
1004
+ }
1005
+ // Accordion의 경우 title prop 추가
1006
+ if (component.name === 'Accordion' && node.children && node.children.length > 0) {
1007
+ const firstTextChild = node.children.find(child => child.type === 'TEXT' || child.characters);
1008
+ if (firstTextChild && firstTextChild.characters) {
1009
+ props.push(`title="${firstTextChild.characters}"`);
1010
+ }
1011
+ }
1012
+ return props.length > 0 ? ` ${props.join(' ')}` : '';
1013
+ }
1014
+ /**
1015
+ * Figma 노드에 대한 적절한 HTML 태그 이름 가져오기
1016
+ */
1017
+ getHTMLTagName(node) {
1018
+ switch (node.type) {
1019
+ case 'TEXT':
1020
+ return 'span';
1021
+ case 'FRAME':
1022
+ return 'div';
1023
+ case 'RECTANGLE':
1024
+ return 'div';
1025
+ case 'ELLIPSE':
1026
+ return 'div';
1027
+ default:
1028
+ return 'div';
1029
+ }
1030
+ }
1031
+ /**
1032
+ * Figma 노드에서 CSS 클래스 이름 생성
1033
+ */
1034
+ generateClassName(node) {
1035
+ const name = node.name
1036
+ .toLowerCase()
1037
+ .replace(/[^a-z0-9]/g, '-')
1038
+ .replace(/-+/g, '-')
1039
+ .replace(/^-|-$/g, '');
1040
+ return `${name}-${node.id.slice(-4)}`;
1041
+ }
1042
+ /**
1043
+ * Figma 노드 속성에서 인라인 스타일 생성
1044
+ */
1045
+ generateInlineStyle(node) {
1046
+ const styles = [];
1047
+ if (node.absoluteBoundingBox) {
1048
+ const { width, height } = node.absoluteBoundingBox;
1049
+ styles.push(`width: ${width}px`);
1050
+ styles.push(`height: ${height}px`);
1051
+ }
1052
+ if (node.cornerRadius) {
1053
+ styles.push(`border-radius: ${node.cornerRadius}px`);
1054
+ }
1055
+ if (node.fills && node.fills.length > 0) {
1056
+ const fill = node.fills[0];
1057
+ if (fill.type === 'SOLID' && fill.color) {
1058
+ const { r, g, b, a } = fill.color;
1059
+ const alpha = a !== undefined ? a : 1;
1060
+ styles.push(`background-color: rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${alpha})`);
1061
+ }
1062
+ }
1063
+ if (node.strokes && node.strokes.length > 0) {
1064
+ const stroke = node.strokes[0];
1065
+ if (stroke.type === 'SOLID' && stroke.color) {
1066
+ const { r, g, b, a } = stroke.color;
1067
+ const alpha = a !== undefined ? a : 1;
1068
+ styles.push(`border: ${stroke.strokeWeight || 1}px solid rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${alpha})`);
1069
+ }
1070
+ }
1071
+ return styles.length > 0 ? ` style="${styles.join('; ')}"` : '';
1072
+ }
1073
+ }