palette-mcp 1.1.2 → 1.2.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.
package/src/server.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Palette MCP Server - 공통 로직
3
- *
3
+ *
4
4
  * Local(stdio) 모드와 Remote(Smithery) 모드에서 공유되는 핵심 기능을 정의합니다.
5
5
  */
6
6
 
@@ -8,44 +8,70 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
8
8
  import {
9
9
  CallToolRequestSchema,
10
10
  ListToolsRequestSchema,
11
+ ListPromptsRequestSchema,
12
+ GetPromptRequestSchema,
13
+ ListResourcesRequestSchema,
14
+ ReadResourceRequestSchema,
11
15
  Tool,
12
16
  } from '@modelcontextprotocol/sdk/types.js';
13
17
  import { readFile } from 'fs/promises';
14
18
  import { FigmaService } from './services/figma.js';
15
- import { DesignSystemService } from './services/design-system.js';
19
+ import { DesignSystemService, type SyncConfig } from './services/design-system.js';
16
20
  import { CodeGenerator, type PreviewType } from './services/code-generator.js';
21
+ import { validateAccess, type AuthResult } from './services/auth.js';
17
22
 
18
23
  // 서버 설정 타입
19
24
  export interface ServerConfig {
20
25
  figmaAccessToken?: string;
21
26
  githubToken?: string;
22
27
  figmaMcpServerUrl?: string;
28
+ /**
29
+ * Figma Desktop MCP 클라이언트 사용 여부
30
+ * Remote 모드(Smithery)에서는 false로 설정해야 함 (로컬호스트 접근 불가)
31
+ */
32
+ useFigmaMcp?: boolean;
33
+ /**
34
+ * 디자인 시스템 컴포넌트 동기화 설정
35
+ */
36
+ syncConfig?: SyncConfig;
37
+ /**
38
+ * 인증 건너뛰기 (로컬 개발용)
39
+ * true로 설정하면 GitHub 조직 멤버십 확인을 건너뜁니다.
40
+ */
41
+ skipAuth?: boolean;
23
42
  }
24
43
 
25
- // Tools 정의
44
+ // Tools 정의 (annotations 포함)
26
45
  export const tools: Tool[] = [
27
46
  {
28
47
  name: 'convert_figma_to_react',
29
- description: 'Figma 디자인을 디자인 시스템을 사용하여 React 컴포넌트로 변환합니다',
48
+ description: 'Figma 디자인을 @dealicious/design-system-react 컴포넌트를 사용하여 React 컴포넌트로 변환합니다. Figma 파일 URL과 컴포넌트 이름을 제공하면, 디자인 시스템 컴포넌트(Button, Text, Tag, Check, Chip 등)를 활용한 React TSX 코드를 생성합니다.',
49
+ annotations: {
50
+ title: 'Figma to React Converter',
51
+ readOnlyHint: false,
52
+ destructiveHint: false,
53
+ idempotentHint: true,
54
+ openWorldHint: true,
55
+ },
30
56
  inputSchema: {
31
57
  type: 'object',
32
58
  properties: {
33
59
  figmaUrl: {
34
60
  type: 'string',
35
- description: 'Figma 파일 URL 또는 파일 ID',
61
+ description: 'Figma 파일의 전체 URL (예: https://www.figma.com/file/ABC123/Design?node-id=1-2) 또는 파일 ID',
36
62
  },
37
63
  nodeId: {
38
64
  type: 'string',
39
- description: '변환할 특정 노드 ID (선택사항)',
65
+ description: '변환할 특정 Figma 노드의 ID. URL에 node-id가 포함되어 있으면 생략 가능합니다.',
40
66
  },
41
67
  componentName: {
42
68
  type: 'string',
43
- description: '생성할 컴포넌트 이름',
69
+ description: '생성될 React 컴포넌트의 이름 (예: ProductCard, LoginForm)',
44
70
  },
45
71
  previewType: {
46
72
  type: 'string',
47
73
  enum: ['html', 'image', 'both'],
48
- description: '미리보기 타입: "html"은 HTML 파일, "image"는 PNG 이미지, "both"는 둘 다 (기본값: "both")',
74
+ description: '미리보기 생성 옵션: "html"은 브라우저에서 볼 수 있는 HTML 파일, "image"는 PNG 스크린샷, "both"는 둘 다 생성 (기본값: "both")',
49
75
  default: 'both',
50
76
  },
51
77
  },
@@ -54,26 +80,33 @@ export const tools: Tool[] = [
54
80
  },
55
81
  {
56
82
  name: 'convert_figma_to_vue',
57
- description: 'Figma 디자인을 디자인 시스템을 사용하여 Vue 컴포넌트로 변환합니다',
83
+ description: 'Figma 디자인을 @dealicious/design-system Vue 컴포넌트를 사용하여 Vue 3 Single File Component로 변환합니다. Figma 파일 URL과 컴포넌트 이름을 제공하면, 디자인 시스템 컴포넌트를 활용한 Vue SFC 코드를 생성합니다.',
84
+ annotations: {
85
+ title: 'Figma to Vue Converter',
86
+ readOnlyHint: false,
87
+ destructiveHint: false,
88
+ idempotentHint: true,
89
+ openWorldHint: true,
90
+ },
58
91
  inputSchema: {
59
92
  type: 'object',
60
93
  properties: {
61
94
  figmaUrl: {
62
95
  type: 'string',
63
- description: 'Figma 파일 URL 또는 파일 ID',
96
+ description: 'Figma 파일의 전체 URL (예: https://www.figma.com/file/ABC123/Design?node-id=1-2) 또는 파일 ID',
64
97
  },
65
98
  nodeId: {
66
99
  type: 'string',
67
- description: '변환할 특정 노드 ID (선택사항)',
100
+ description: '변환할 특정 Figma 노드의 ID. URL에 node-id가 포함되어 있으면 생략 가능합니다.',
68
101
  },
69
102
  componentName: {
70
103
  type: 'string',
71
- description: '생성할 컴포넌트 이름',
104
+ description: '생성될 Vue 컴포넌트의 이름 (예: ProductCard, LoginForm)',
72
105
  },
73
106
  previewType: {
74
107
  type: 'string',
75
108
  enum: ['html', 'image', 'both'],
76
- description: '미리보기 타입: "html"은 HTML 파일, "image"는 PNG 이미지, "both"는 둘 다 (기본값: "both")',
109
+ description: '미리보기 생성 옵션: "html"은 브라우저에서 볼 수 있는 HTML 파일, "image"는 PNG 스크린샷, "both"는 둘 다 생성 (기본값: "both")',
77
110
  default: 'both',
78
111
  },
79
112
  },
@@ -82,14 +115,21 @@ export const tools: Tool[] = [
82
115
  },
83
116
  {
84
117
  name: 'list_design_system_components',
85
- description: '디자인 시스템에서 사용 가능한 컴포넌트 목록을 조회합니다',
118
+ description: '@dealicious/design-system에서 사용 가능한 모든 UI 컴포넌트 목록을 조회합니다. 각 컴포넌트의 이름, 설명, import 경로, 사용 가능한 props를 반환합니다.',
119
+ annotations: {
120
+ title: 'Design System Component List',
121
+ readOnlyHint: true,
122
+ destructiveHint: false,
123
+ idempotentHint: true,
124
+ openWorldHint: false,
125
+ },
86
126
  inputSchema: {
87
127
  type: 'object',
88
128
  properties: {
89
129
  framework: {
90
130
  type: 'string',
91
131
  enum: ['react', 'vue'],
92
- description: '컴포넌트를 조회할 프레임워크',
132
+ description: '컴포넌트를 조회할 프레임워크: "react"는 design-system-react, "vue"는 design-system 패키지',
93
133
  },
94
134
  },
95
135
  required: ['framework'],
@@ -97,13 +137,20 @@ export const tools: Tool[] = [
97
137
  },
98
138
  {
99
139
  name: 'analyze_figma_file',
100
- description: 'Figma 파일 구조와 사용 가능한 컴포넌트를 분석합니다',
140
+ description: 'Figma 파일의 구조를 분석하여 페이지, 프레임, 컴포넌트 계층을 파악합니다. 변환하기 전에 파일 구조를 이해하는 유용합니다.',
141
+ annotations: {
142
+ title: 'Figma File Analyzer',
143
+ readOnlyHint: true,
144
+ destructiveHint: false,
145
+ idempotentHint: true,
146
+ openWorldHint: true,
147
+ },
101
148
  inputSchema: {
102
149
  type: 'object',
103
150
  properties: {
104
151
  figmaUrl: {
105
152
  type: 'string',
106
- description: 'Figma 파일 URL 또는 파일 ID',
153
+ description: '분석할 Figma 파일의 전체 URL 또는 파일 ID',
107
154
  },
108
155
  },
109
156
  required: ['figmaUrl'],
@@ -111,6 +158,58 @@ export const tools: Tool[] = [
111
158
  },
112
159
  ];
113
160
 
161
+ // Prompts 정의
162
+ const prompts = [
163
+ {
164
+ name: 'convert-design-to-component',
165
+ description: 'Figma 디자인을 React 또는 Vue 컴포넌트로 변환하는 가이드 프롬프트',
166
+ arguments: [
167
+ {
168
+ name: 'figmaUrl',
169
+ description: 'Figma 파일 URL',
170
+ required: true,
171
+ },
172
+ {
173
+ name: 'framework',
174
+ description: '프레임워크 선택 (react 또는 vue)',
175
+ required: true,
176
+ },
177
+ {
178
+ name: 'componentName',
179
+ description: '컴포넌트 이름',
180
+ required: true,
181
+ },
182
+ ],
183
+ },
184
+ {
185
+ name: 'explore-design-system',
186
+ description: '디자인 시스템 컴포넌트 탐색 가이드',
187
+ arguments: [
188
+ {
189
+ name: 'framework',
190
+ description: '프레임워크 선택 (react 또는 vue)',
191
+ required: false,
192
+ },
193
+ ],
194
+ },
195
+ ];
196
+
197
+ // Resources 정의
198
+ const resources = [
199
+ {
200
+ uri: 'palette://design-system/react/components',
201
+ name: 'React Design System Components',
202
+ description: '@dealicious/design-system-react에서 사용 가능한 컴포넌트 목록',
203
+ mimeType: 'application/json',
204
+ },
205
+ {
206
+ uri: 'palette://design-system/vue/components',
207
+ name: 'Vue Design System Components',
208
+ description: '@dealicious/design-system에서 사용 가능한 컴포넌트 목록',
209
+ mimeType: 'application/json',
210
+ },
211
+ ];
212
+
114
213
  /**
115
214
  * MCP 서버를 생성하고 핸들러를 등록합니다.
116
215
  * Local 모드와 Remote 모드에서 공통으로 사용됩니다.
@@ -127,27 +226,73 @@ export function createPaletteServer(config: ServerConfig = {}): Server {
127
226
  process.env.FIGMA_MCP_SERVER_URL = config.figmaMcpServerUrl;
128
227
  }
129
228
 
130
- // MCP 서버 초기화
131
- const server = new Server({
132
- name: 'palette',
133
- version: '1.0.0',
134
- });
229
+ // MCP 서버 초기화 (모든 capabilities 활성화)
230
+ const server = new Server(
231
+ {
232
+ name: 'palette',
233
+ version: '1.0.0',
234
+ },
235
+ {
236
+ capabilities: {
237
+ tools: {}, // tools 기능 활성화
238
+ prompts: {}, // prompts 기능 활성화
239
+ resources: {}, // resources 기능 활성화
240
+ },
241
+ }
242
+ );
135
243
 
136
244
  // 서비스 초기화
137
- const figmaService = new FigmaService();
138
- const designSystemService = new DesignSystemService();
245
+ // Remote 모드에서는 Figma Desktop MCP 클라이언트를 사용하지 않음 (로컬호스트 접근 불가)
246
+ const useFigmaMcp = config.useFigmaMcp !== undefined ? config.useFigmaMcp : true;
247
+ const figmaService = new FigmaService(useFigmaMcp, config.figmaMcpServerUrl);
248
+ const designSystemService = new DesignSystemService(config.syncConfig);
139
249
  const codeGenerator = new CodeGenerator(designSystemService);
140
250
 
141
- // 도구 목록 핸들러
251
+ // 인증 상태 캐시
252
+ let authResult: AuthResult | null = null;
253
+
254
+ /**
255
+ * 도구 실행 전 인증 확인
256
+ * skipAuth가 true이면 인증을 건너뜁니다.
257
+ */
258
+ async function ensureAuthenticated(): Promise<void> {
259
+ // 로컬 개발 모드에서는 인증 건너뛰기
260
+ if (config.skipAuth) {
261
+ console.error('[Palette Auth] ⚠️ 인증 건너뛰기 모드 (skipAuth=true)');
262
+ return;
263
+ }
264
+
265
+ // 이미 인증되었으면 건너뛰기
266
+ if (authResult?.authorized) {
267
+ return;
268
+ }
269
+
270
+ // 인증 수행
271
+ const githubToken = config.githubToken || process.env.GITHUB_TOKEN;
272
+ authResult = await validateAccess(githubToken);
273
+
274
+ if (!authResult.authorized) {
275
+ throw new Error(authResult.error || '인증에 실패했습니다.');
276
+ }
277
+ }
278
+
279
+ // 디자인 시스템 컴포넌트 비동기 초기화 (백그라운드에서 실행)
280
+ designSystemService.initialize().catch((error) => {
281
+ console.error('[Palette] 디자인 시스템 초기화 실패:', error);
282
+ });
283
+
284
+ // ===== Tools 핸들러 =====
142
285
  server.setRequestHandler(ListToolsRequestSchema, async () => {
143
286
  return { tools };
144
287
  });
145
288
 
146
- // 도구 실행 핸들러
147
289
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
148
290
  const { name, arguments: args } = request.params;
149
291
 
150
292
  try {
293
+ // 🔐 도구 실행 전 인증 확인
294
+ await ensureAuthenticated();
295
+
151
296
  switch (name) {
152
297
  case 'convert_figma_to_react': {
153
298
  const { figmaUrl, nodeId, componentName, previewType = 'both' } = args as {
@@ -297,5 +442,95 @@ export function createPaletteServer(config: ServerConfig = {}): Server {
297
442
  }
298
443
  });
299
444
 
445
+ // ===== Prompts 핸들러 =====
446
+ server.setRequestHandler(ListPromptsRequestSchema, async () => {
447
+ return { prompts };
448
+ });
449
+
450
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
451
+ const { name, arguments: args } = request.params;
452
+
453
+ switch (name) {
454
+ case 'convert-design-to-component': {
455
+ const figmaUrl = args?.figmaUrl || '<FIGMA_URL>';
456
+ const framework = args?.framework || 'react';
457
+ const componentName = args?.componentName || 'MyComponent';
458
+
459
+ return {
460
+ description: 'Figma 디자인을 컴포넌트로 변환하는 프롬프트',
461
+ messages: [
462
+ {
463
+ role: 'user',
464
+ content: {
465
+ type: 'text',
466
+ text: `Figma 디자인을 ${framework} 컴포넌트로 변환해주세요.\n\n- Figma URL: ${figmaUrl}\n- 컴포넌트 이름: ${componentName}\n\n디자인 시스템 컴포넌트를 최대한 활용해주세요.`,
467
+ },
468
+ },
469
+ ],
470
+ };
471
+ }
472
+
473
+ case 'explore-design-system': {
474
+ const framework = args?.framework || 'react';
475
+
476
+ return {
477
+ description: '디자인 시스템 탐색 프롬프트',
478
+ messages: [
479
+ {
480
+ role: 'user',
481
+ content: {
482
+ type: 'text',
483
+ text: `@dealicious/design-system${framework === 'react' ? '-react' : ''} 패키지에서 사용 가능한 컴포넌트 목록을 보여주세요. 각 컴포넌트의 사용법과 예제도 함께 알려주세요.`,
484
+ },
485
+ },
486
+ ],
487
+ };
488
+ }
489
+
490
+ default:
491
+ throw new Error(`Unknown prompt: ${name}`);
492
+ }
493
+ });
494
+
495
+ // ===== Resources 핸들러 =====
496
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
497
+ return { resources };
498
+ });
499
+
500
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
501
+ const { uri } = request.params;
502
+
503
+ switch (uri) {
504
+ case 'palette://design-system/react/components': {
505
+ const components = await designSystemService.getAvailableComponents('react');
506
+ return {
507
+ contents: [
508
+ {
509
+ uri,
510
+ mimeType: 'application/json',
511
+ text: JSON.stringify(components, null, 2),
512
+ },
513
+ ],
514
+ };
515
+ }
516
+
517
+ case 'palette://design-system/vue/components': {
518
+ const components = await designSystemService.getAvailableComponents('vue');
519
+ return {
520
+ contents: [
521
+ {
522
+ uri,
523
+ mimeType: 'application/json',
524
+ text: JSON.stringify(components, null, 2),
525
+ },
526
+ ],
527
+ };
528
+ }
529
+
530
+ default:
531
+ throw new Error(`Unknown resource: ${uri}`);
532
+ }
533
+ });
534
+
300
535
  return server;
301
536
  }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * 인증 및 권한 검증 서비스
3
+ *
4
+ * dealicious-inc 조직 멤버십을 확인하여 서비스 접근을 제어합니다.
5
+ */
6
+
7
+ import { Octokit } from '@octokit/rest';
8
+
9
+ // 허용된 GitHub 조직 목록
10
+ const ALLOWED_ORGANIZATIONS = ['dealicious-inc'];
11
+
12
+ // 인증 상태 캐시 (토큰별로 검증 결과를 캐시)
13
+ const authCache = new Map<string, { valid: boolean; username: string; checkedAt: Date }>();
14
+
15
+ // 캐시 유효 시간 (1시간)
16
+ const CACHE_TTL_MS = 60 * 60 * 1000;
17
+
18
+ /**
19
+ * GitHub 토큰으로 사용자 정보 조회
20
+ */
21
+ async function getAuthenticatedUser(octokit: Octokit): Promise<{ login: string; email: string | null }> {
22
+ const { data } = await octokit.users.getAuthenticated();
23
+ return { login: data.login, email: data.email };
24
+ }
25
+
26
+ /**
27
+ * 사용자가 허용된 조직의 멤버인지 확인
28
+ */
29
+ async function checkOrganizationMembership(octokit: Octokit, org: string): Promise<boolean> {
30
+ try {
31
+ await octokit.orgs.getMembershipForAuthenticatedUser({ org });
32
+ return true;
33
+ } catch (error: any) {
34
+ // 404: 멤버가 아님, 403: 권한 없음
35
+ if (error.status === 404 || error.status === 403) {
36
+ return false;
37
+ }
38
+ throw error;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * 캐시된 인증 결과 확인
44
+ */
45
+ function getCachedAuth(githubToken: string): { valid: boolean; username: string } | null {
46
+ const cached = authCache.get(githubToken);
47
+ if (!cached) return null;
48
+
49
+ // 캐시 만료 확인
50
+ const now = new Date();
51
+ if (now.getTime() - cached.checkedAt.getTime() > CACHE_TTL_MS) {
52
+ authCache.delete(githubToken);
53
+ return null;
54
+ }
55
+
56
+ return { valid: cached.valid, username: cached.username };
57
+ }
58
+
59
+ /**
60
+ * 인증 결과 캐시 저장
61
+ */
62
+ function setCachedAuth(githubToken: string, valid: boolean, username: string): void {
63
+ authCache.set(githubToken, { valid, username, checkedAt: new Date() });
64
+ }
65
+
66
+ export interface AuthResult {
67
+ authorized: boolean;
68
+ username?: string;
69
+ organization?: string;
70
+ error?: string;
71
+ }
72
+
73
+ /**
74
+ * GitHub 조직 멤버십을 확인하여 서비스 접근 권한을 검증합니다.
75
+ *
76
+ * @param githubToken GitHub Personal Access Token
77
+ * @returns 인증 결과
78
+ */
79
+ export async function validateAccess(githubToken?: string): Promise<AuthResult> {
80
+ // GITHUB_TOKEN이 없으면 거부
81
+ if (!githubToken) {
82
+ return {
83
+ authorized: false,
84
+ error: 'GITHUB_TOKEN이 필요합니다. dealicious-inc 조직 멤버만 Palette MCP를 사용할 수 있습니다.',
85
+ };
86
+ }
87
+
88
+ // 캐시 확인
89
+ const cached = getCachedAuth(githubToken);
90
+ if (cached) {
91
+ if (cached.valid) {
92
+ return {
93
+ authorized: true,
94
+ username: cached.username,
95
+ organization: ALLOWED_ORGANIZATIONS[0],
96
+ };
97
+ } else {
98
+ return {
99
+ authorized: false,
100
+ username: cached.username,
101
+ error: `사용자 '${cached.username}'은(는) dealicious-inc 조직 멤버가 아닙니다.`,
102
+ };
103
+ }
104
+ }
105
+
106
+ // GitHub API로 검증
107
+ const octokit = new Octokit({ auth: githubToken });
108
+
109
+ try {
110
+ // 사용자 정보 조회
111
+ const user = await getAuthenticatedUser(octokit);
112
+
113
+ // 조직 멤버십 확인
114
+ for (const org of ALLOWED_ORGANIZATIONS) {
115
+ const isMember = await checkOrganizationMembership(octokit, org);
116
+
117
+ if (isMember) {
118
+ // 캐시 저장
119
+ setCachedAuth(githubToken, true, user.login);
120
+
121
+ console.error(`[Palette Auth] ✅ 인증 성공: ${user.login} (${org})`);
122
+ return {
123
+ authorized: true,
124
+ username: user.login,
125
+ organization: org,
126
+ };
127
+ }
128
+ }
129
+
130
+ // 어떤 조직에도 속하지 않음
131
+ setCachedAuth(githubToken, false, user.login);
132
+
133
+ console.error(`[Palette Auth] ❌ 인증 실패: ${user.login} - 허용된 조직 멤버 아님`);
134
+ return {
135
+ authorized: false,
136
+ username: user.login,
137
+ error: `사용자 '${user.login}'은(는) dealicious-inc 조직 멤버가 아닙니다. 조직 관리자에게 문의하세요.`,
138
+ };
139
+ } catch (error: any) {
140
+ // 토큰 유효성 검사 실패
141
+ if (error.status === 401) {
142
+ return {
143
+ authorized: false,
144
+ error: 'GITHUB_TOKEN이 유효하지 않습니다. 토큰을 확인해주세요.',
145
+ };
146
+ }
147
+
148
+ // 기타 오류
149
+ console.error('[Palette Auth] 인증 중 오류 발생:', error.message);
150
+ return {
151
+ authorized: false,
152
+ error: `인증 중 오류가 발생했습니다: ${error.message}`,
153
+ };
154
+ }
155
+ }
156
+
157
+ /**
158
+ * 인증 캐시 초기화 (테스트용)
159
+ */
160
+ export function clearAuthCache(): void {
161
+ authCache.clear();
162
+ }
@@ -4,10 +4,22 @@ import { saveFile, saveBinaryFile, saveMetadata, generateRequestId, getRequestFo
4
4
 
5
5
  // puppeteer는 optional dependency로, 없으면 이미지 프리뷰 기능이 비활성화됨
6
6
  let puppeteer: any = null;
7
- try {
8
- puppeteer = (await import('puppeteer')).default;
9
- } catch (e) {
10
- console.warn('puppeteer를 로드할 없습니다. 이미지 프리뷰 기능이 비활성화됩니다.');
7
+ let puppeteerLoaded = false;
8
+
9
+ /**
10
+ * puppeteer를 lazy loading으로 로드
11
+ */
12
+ async function loadPuppeteer(): Promise<any> {
13
+ if (puppeteerLoaded) return puppeteer;
14
+
15
+ try {
16
+ puppeteer = (await import('puppeteer')).default;
17
+ } catch (e) {
18
+ console.warn('puppeteer를 로드할 수 없습니다. 이미지 프리뷰 기능이 비활성화됩니다.');
19
+ puppeteer = null;
20
+ }
21
+ puppeteerLoaded = true;
22
+ return puppeteer;
11
23
  }
12
24
 
13
25
  export interface GeneratedComponent {
@@ -91,13 +103,16 @@ export class CodeGenerator {
91
103
  componentName: string,
92
104
  requestId: string
93
105
  ): Promise<string> {
106
+ // puppeteer를 lazy loading으로 로드
107
+ const pptr = await loadPuppeteer();
108
+
94
109
  // puppeteer가 없으면 이미지 생성 불가
95
- if (!puppeteer) {
110
+ if (!pptr) {
96
111
  console.warn('puppeteer가 설치되지 않아 이미지 프리뷰를 생성할 수 없습니다.');
97
112
  return '';
98
113
  }
99
114
 
100
- const browser = await puppeteer.launch({
115
+ const browser = await pptr.launch({
101
116
  headless: true,
102
117
  args: ['--no-sandbox', '--disable-setuid-sandbox']
103
118
  });