Skip to main content
Glama
figma.ts12.3 kB
import axios from 'axios'; import { FigmaMCPClient } from '../utils/figma-mcp-client.js'; export interface FigmaNode { id: string; name: string; type: string; visible?: boolean; children?: FigmaNode[]; absoluteBoundingBox?: { x: number; y: number; width: number; height: number; }; fills?: Array<{ type: string; color?: { r: number; g: number; b: number; a: number; }; gradientStops?: Array<{ position: number; color: { r: number; g: number; b: number; a: number; }; }>; }>; strokes?: Array<{ type: string; color?: { r: number; g: number; b: number; a: number; }; strokeWeight?: number; }>; cornerRadius?: number; characters?: string; style?: { fontFamily?: string; fontSize?: number; fontWeight?: number; textAlignHorizontal?: string; textAlignVertical?: string; }; layoutMode?: 'NONE' | 'HORIZONTAL' | 'VERTICAL'; primaryAxisSizingMode?: 'FIXED' | 'AUTO'; counterAxisSizingMode?: 'FIXED' | 'AUTO'; paddingLeft?: number; paddingRight?: number; paddingTop?: number; paddingBottom?: number; itemSpacing?: number; } export interface FigmaFile { document: FigmaNode; components: Record<string, FigmaNode>; styles: Record<string, any>; name: string; lastModified: string; thumbnailUrl: string; } export interface FigmaAnalysis { totalNodes: number; componentCount: number; frameCount: number; textCount: number; availableComponents: string[]; suggestedMappings: Array<{ figmaComponent: string; designSystemComponent: string; confidence: number; }>; } export class FigmaService { private accessToken: string; private baseUrl = 'https://api.figma.com/v1'; private mcpClient: FigmaMCPClient | null = null; private useMCP: boolean; constructor(useMCP: boolean = true, mcpBaseUrl?: string) { this.accessToken = process.env.FIGMA_ACCESS_TOKEN || ''; this.useMCP = useMCP; if (useMCP) { const mcpUrl = mcpBaseUrl || process.env.FIGMA_MCP_SERVER_URL || 'http://127.0.0.1:3845/mcp'; this.mcpClient = new FigmaMCPClient(mcpUrl); } if (!this.accessToken) { console.warn('환경 변수에서 FIGMA_ACCESS_TOKEN을 찾을 수 없습니다.'); } } /** * Figma URL에서 파일 ID 추출 */ private extractFileId(url: string): string { // /file/ 또는 /design/ 경로에서 파일 ID 추출 const match = url.match(/\/(?:file|design)\/([a-zA-Z0-9]+)/); if (match) { return match[1]; } // 이미 파일 ID인 경우 if (/^[a-zA-Z0-9]+$/.test(url)) { return url; } throw new Error('잘못된 Figma URL 형식입니다.'); } /** * Figma URL에서 node-id 추출 */ extractNodeId(url: string): string | undefined { const match = url.match(/[?&]node-id=([^&]+)/); if (match) { // node-id는 URL 인코딩되어 있을 수 있으므로 디코딩 return decodeURIComponent(match[1]); } return undefined; } /** * MCP 응답을 FigmaFile 형식으로 변환 */ private transformMCPResponseToFigmaFile(mcpData: any): FigmaFile { // MCP 응답 형식에 따라 변환 로직 구현 // Figma MCP 서버의 응답 구조에 맞게 조정 필요 if (!mcpData) { throw new Error('MCP 응답 데이터가 없습니다.'); } // MCP 응답이 이미 FigmaFile 형식인 경우 if (mcpData.document) { return { document: mcpData.document, components: mcpData.components || {}, styles: mcpData.styles || {}, name: mcpData.name || 'Untitled', lastModified: mcpData.lastModified || new Date().toISOString(), thumbnailUrl: mcpData.thumbnailUrl || '', }; } // MCP 응답이 다른 형식인 경우 변환 // content 배열에서 데이터 추출 (MCP 도구 응답 형식) if (mcpData.content && Array.isArray(mcpData.content)) { const textContent = mcpData.content.find((c: any) => c.type === 'text'); if (textContent) { try { const parsed = JSON.parse(textContent.text); return this.transformMCPResponseToFigmaFile(parsed); } catch { // JSON이 아닌 경우 처리 } } } // 기본 구조로 변환 시도 return { document: mcpData.document || mcpData.node || { id: 'root', name: 'Document', type: 'DOCUMENT', children: [] }, components: mcpData.components || {}, styles: mcpData.styles || {}, name: mcpData.name || 'Untitled', lastModified: mcpData.lastModified || new Date().toISOString(), thumbnailUrl: mcpData.thumbnailUrl || '', }; } /** * Figma 파일 데이터 가져오기 * MCP 서버를 우선 사용하고, 실패 시 기존 REST API로 폴백 */ async getFigmaData(url: string, nodeId?: string): Promise<FigmaFile> { const fileId = this.extractFileId(url); // MCP 클라이언트 사용 시도 if (this.useMCP && this.mcpClient !== null) { try { const isAvailable = await this.mcpClient.isAvailable(); if (isAvailable) { const mcpData = nodeId ? await this.mcpClient.getNodeData(fileId, nodeId) : await this.mcpClient.getFileData(fileId, nodeId); if (mcpData) { try { return this.transformMCPResponseToFigmaFile(mcpData); } catch (error) { console.warn('MCP 응답 변환 실패, REST API로 폴백:', error); // 폴백으로 REST API 사용 } } } } catch (error) { console.warn('Figma MCP 서버 연결 실패, REST API로 폴백:', error); // 폴백으로 REST API 사용 } } // REST API 폴백 if (!this.accessToken) { throw new Error('Figma 액세스 토큰이 필요합니다. MCP 서버도 사용할 수 없습니다.'); } try { const response = await axios.get(`${this.baseUrl}/files/${fileId}`, { headers: { 'X-Figma-Token': this.accessToken, }, params: { ids: nodeId || undefined, }, }); return { document: response.data.document, components: response.data.components || {}, styles: response.data.styles || {}, name: response.data.name, lastModified: response.data.lastModified, thumbnailUrl: response.data.thumbnailUrl, }; } catch (error) { if (axios.isAxiosError(error)) { throw new Error(`Figma API error: ${error.response?.data?.message || error.message}`); } throw error; } } /** * Figma 파일 구조 분석 */ async analyzeFigmaFile(url: string): Promise<string> { const fileData = await this.getFigmaData(url); const analysis = this.analyzeFileStructure(fileData); return this.formatAnalysis(analysis); } /** * 파일 구조 분석 및 컴포넌트 정보 추출 */ private analyzeFileStructure(file: FigmaFile): FigmaAnalysis { const stats = this.countNodes(file.document); const availableComponents = Object.keys(file.components); return { totalNodes: stats.total, componentCount: stats.components, frameCount: stats.frames, textCount: stats.text, availableComponents, suggestedMappings: this.suggestComponentMappings(availableComponents), }; } /** * 파일에 있는 다른 유형의 노드 수 세기 */ private countNodes(node: FigmaNode): { total: number; components: number; frames: number; text: number; } { let total = 1; let components = node.type === 'COMPONENT' ? 1 : 0; let frames = node.type === 'FRAME' ? 1 : 0; let text = node.type === 'TEXT' ? 1 : 0; if (node.children) { for (const child of node.children) { const childStats = this.countNodes(child); total += childStats.total; components += childStats.components; frames += childStats.frames; text += childStats.text; } } return { total, components, frames, text }; } /** * Figma 컴포넌트와 디자인 시스템 컴포넌트 간의 매핑 제안 */ private suggestComponentMappings(figmaComponents: string[]): Array<{ figmaComponent: string; designSystemComponent: string; confidence: number; }> { const mappings: Array<{ figmaComponent: string; designSystemComponent: string; confidence: number; }> = []; // 일반적인 컴포넌트 이름 매핑 const commonMappings: Record<string, string[]> = { 'button': ['Button', 'Btn', 'PrimaryButton', 'SecondaryButton'], 'input': ['Input', 'TextField', 'TextInput'], 'card': ['Card', 'Panel', 'Container'], 'modal': ['Modal', 'Dialog', 'Popup'], 'header': ['Header', 'Navbar', 'Navigation'], 'footer': ['Footer', 'BottomBar'], 'sidebar': ['Sidebar', 'Drawer', 'Navigation'], 'form': ['Form', 'FormGroup'], 'table': ['Table', 'DataTable'], 'list': ['List', 'ListItem'], 'avatar': ['Avatar', 'ProfileImage'], 'badge': ['Badge', 'Tag', 'Label'], 'tooltip': ['Tooltip', 'Popover'], 'dropdown': ['Dropdown', 'Select'], 'checkbox': ['Checkbox', 'CheckBox'], 'radio': ['Radio', 'RadioButton'], 'switch': ['Switch', 'Toggle'], 'slider': ['Slider', 'Range'], 'progress': ['Progress', 'ProgressBar'], 'spinner': ['Spinner', 'Loader'], 'alert': ['Alert', 'Notification'], 'breadcrumb': ['Breadcrumb', 'Breadcrumbs'], 'pagination': ['Pagination', 'Pager'], 'tabs': ['Tabs', 'TabList'], 'accordion': ['Accordion', 'Collapse'], 'carousel': ['Carousel', 'Slider'], 'stepper': ['Stepper', 'Steps'], }; for (const figmaComponent of figmaComponents) { const lowerName = figmaComponent.toLowerCase(); for (const [key, possibleNames] of Object.entries(commonMappings)) { for (const possibleName of possibleNames) { if (lowerName.includes(key) || lowerName.includes(possibleName.toLowerCase())) { mappings.push({ figmaComponent, designSystemComponent: possibleName, confidence: 0.8, }); break; } } } } return mappings; } /** * 분석 결과 표시 형식 지정 */ private formatAnalysis(analysis: FigmaAnalysis): string { let result = `## Figma File Analysis\n\n`; result += `**File Statistics:**\n`; result += `- Total Nodes: ${analysis.totalNodes}\n`; result += `- Components: ${analysis.componentCount}\n`; result += `- Frames: ${analysis.frameCount}\n`; result += `- Text Elements: ${analysis.textCount}\n\n`; if (analysis.availableComponents.length > 0) { result += `**Available Components:**\n`; analysis.availableComponents.forEach(comp => { result += `- ${comp}\n`; }); result += `\n`; } if (analysis.suggestedMappings.length > 0) { result += `**Suggested Component Mappings:**\n`; analysis.suggestedMappings.forEach(mapping => { result += `- ${mapping.figmaComponent} → ${mapping.designSystemComponent} (${Math.round(mapping.confidence * 100)}% confidence)\n`; }); } return result; } /** * Figma 파일에서 디자인 토큰 추출 */ extractDesignTokens(file: FigmaFile): Record<string, any> { const tokens: Record<string, any> = { colors: {}, typography: {}, spacing: {}, borderRadius: {}, }; // 스타일에서 색상 추출 for (const [key, style] of Object.entries(file.styles)) { if (style.styleType === 'FILL') { tokens.colors[key] = style.description || key; } } // 타이포그래피 추출 for (const [key, style] of Object.entries(file.styles)) { if (style.styleType === 'TEXT') { tokens.typography[key] = { fontFamily: style.fontFamily, fontSize: style.fontSize, fontWeight: style.fontWeight, }; } } return tokens; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Opti-kjh/palatte'

If you have feedback or need assistance with the MCP directory API, please join our Discord server