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