leemage-sdk 0.1.1

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/dist/index.js ADDED
@@ -0,0 +1,507 @@
1
+ 'use strict';
2
+
3
+ // src/errors/index.ts
4
+ var LeemageError = class _LeemageError extends Error {
5
+ constructor(message, status, errors) {
6
+ super(message);
7
+ this.name = "LeemageError";
8
+ this.status = status;
9
+ this.errors = errors;
10
+ }
11
+ static fromResponse(response, status) {
12
+ switch (status) {
13
+ case 400:
14
+ return new ValidationError(response.message, status, response.errors);
15
+ case 401:
16
+ return new AuthenticationError(
17
+ response.message,
18
+ status,
19
+ response.errors
20
+ );
21
+ case 403:
22
+ return new PermissionDeniedError(
23
+ response.message,
24
+ status,
25
+ response.errors
26
+ );
27
+ case 404:
28
+ return new NotFoundError(response.message, status, response.errors);
29
+ case 429:
30
+ return new RateLimitError(response.message, status, response.errors);
31
+ case 413:
32
+ return new FileTooLargeError(response.message, status, response.errors);
33
+ case 500:
34
+ return new ServerError(response.message, status, response.errors);
35
+ default:
36
+ return new _LeemageError(response.message, status, response.errors);
37
+ }
38
+ }
39
+ };
40
+ var AuthenticationError = class extends LeemageError {
41
+ constructor(message = "\uC778\uC99D\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. API \uD0A4\uB97C \uD655\uC778\uD574\uC8FC\uC138\uC694.", status = 401, errors) {
42
+ super(message, status, errors);
43
+ this.name = "AuthenticationError";
44
+ }
45
+ };
46
+ var NotFoundError = class extends LeemageError {
47
+ constructor(message = "\uB9AC\uC18C\uC2A4\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.", status = 404, errors) {
48
+ super(message, status, errors);
49
+ this.name = "NotFoundError";
50
+ }
51
+ };
52
+ var PermissionDeniedError = class extends LeemageError {
53
+ constructor(message = "\uC694\uCCAD \uAD8C\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.", status = 403, errors) {
54
+ super(message, status, errors);
55
+ this.name = "PermissionDeniedError";
56
+ }
57
+ };
58
+ var ValidationError = class extends LeemageError {
59
+ constructor(message = "\uC798\uBABB\uB41C \uC694\uCCAD\uC785\uB2C8\uB2E4.", status = 400, errors) {
60
+ super(message, status, errors);
61
+ this.name = "ValidationError";
62
+ }
63
+ };
64
+ var RateLimitError = class extends LeemageError {
65
+ constructor(message = "\uC694\uCCAD \uD55C\uB3C4\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.", status = 429, errors, retryAfter) {
66
+ super(message, status, errors);
67
+ this.name = "RateLimitError";
68
+ this.retryAfter = retryAfter;
69
+ }
70
+ };
71
+ var FileTooLargeError = class extends LeemageError {
72
+ constructor(message = "\uD30C\uC77C \uD06C\uAE30\uAC00 \uC81C\uD55C\uC744 \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.", status = 413, errors) {
73
+ super(message, status, errors);
74
+ this.name = "FileTooLargeError";
75
+ }
76
+ };
77
+ var ServerError = class extends LeemageError {
78
+ constructor(message = "\uC11C\uBC84 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.", status = 500, errors) {
79
+ super(message, status, errors);
80
+ this.name = "ServerError";
81
+ }
82
+ };
83
+ var NetworkError = class extends LeemageError {
84
+ constructor(message = "\uB124\uD2B8\uC6CC\uD06C \uC5F0\uACB0\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.") {
85
+ super(message, 0);
86
+ this.name = "NetworkError";
87
+ }
88
+ };
89
+
90
+ // src/utils/fetch.ts
91
+ var FetchClient = class {
92
+ constructor(options) {
93
+ let parsedBaseUrl;
94
+ try {
95
+ parsedBaseUrl = new URL(options.baseUrl);
96
+ } catch {
97
+ throw new Error("baseUrl\uC740 \uC62C\uBC14\uB978 \uC808\uB300 URL\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4.");
98
+ }
99
+ const isHttps = parsedBaseUrl.protocol === "https:";
100
+ const isHttp = parsedBaseUrl.protocol === "http:";
101
+ if (!isHttps && !(options.allowInsecureHttp && isHttp)) {
102
+ throw new Error(
103
+ "\uBCF4\uC548\uC744 \uC704\uD574 baseUrl\uC740 https \uD504\uB85C\uD1A0\uCF5C\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4. \uB85C\uCEEC \uAC1C\uBC1C \uD658\uACBD\uC5D0\uC11C\uB294 allowInsecureHttp \uC635\uC158\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4."
104
+ );
105
+ }
106
+ this.baseUrl = parsedBaseUrl.toString().replace(/\/$/, "");
107
+ this.apiKey = options.apiKey;
108
+ this.timeout = options.timeout ?? 3e4;
109
+ if (!Number.isFinite(this.timeout) || this.timeout <= 0) {
110
+ throw new Error("timeout\uC740 0\uBCF4\uB2E4 \uD070 \uC22B\uC790\uC5EC\uC57C \uD569\uB2C8\uB2E4.");
111
+ }
112
+ }
113
+ /**
114
+ * API 요청 실행
115
+ */
116
+ async request(path, options = {}) {
117
+ const url = `${this.baseUrl}${path}`;
118
+ const { method = "GET", body, headers = {} } = options;
119
+ const normalizedHeaders = this.createRequestHeaders(body, headers);
120
+ const controller = new AbortController();
121
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
122
+ try {
123
+ const response = await fetch(url, {
124
+ method,
125
+ headers: normalizedHeaders,
126
+ body: body ? JSON.stringify(body) : void 0,
127
+ signal: controller.signal
128
+ });
129
+ clearTimeout(timeoutId);
130
+ if (!response.ok) {
131
+ const errorData = await this.parseErrorResponse(response);
132
+ const retryAfter = this.parseRetryAfterSeconds(response);
133
+ if (response.status === 403) {
134
+ throw new PermissionDeniedError(
135
+ errorData.message,
136
+ response.status,
137
+ errorData.errors
138
+ );
139
+ }
140
+ if (response.status === 429) {
141
+ throw new RateLimitError(
142
+ errorData.message,
143
+ response.status,
144
+ errorData.errors,
145
+ retryAfter
146
+ );
147
+ }
148
+ throw LeemageError.fromResponse(errorData, response.status);
149
+ }
150
+ if (response.status === 204) {
151
+ return void 0;
152
+ }
153
+ const contentLength = response.headers?.get?.("content-length");
154
+ if (contentLength === "0") {
155
+ return void 0;
156
+ }
157
+ if (typeof response.text === "function") {
158
+ const responseText = await response.text();
159
+ if (!responseText) {
160
+ return void 0;
161
+ }
162
+ try {
163
+ return JSON.parse(responseText);
164
+ } catch {
165
+ return responseText;
166
+ }
167
+ }
168
+ if (typeof response.json === "function") {
169
+ return await response.json();
170
+ }
171
+ return void 0;
172
+ } catch (error) {
173
+ clearTimeout(timeoutId);
174
+ if (error instanceof LeemageError) {
175
+ throw error;
176
+ }
177
+ if (error instanceof Error) {
178
+ if (error.name === "AbortError") {
179
+ throw new NetworkError("\uC694\uCCAD \uC2DC\uAC04\uC774 \uCD08\uACFC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
180
+ }
181
+ throw new NetworkError(error.message);
182
+ }
183
+ throw new NetworkError("\uC54C \uC218 \uC5C6\uB294 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.");
184
+ }
185
+ }
186
+ createRequestHeaders(body, headers) {
187
+ const normalizedHeaders = {
188
+ Authorization: `Bearer ${this.apiKey}`,
189
+ ...headers
190
+ };
191
+ const hasContentTypeHeader = Object.keys(normalizedHeaders).some(
192
+ (key) => key.toLowerCase() === "content-type"
193
+ );
194
+ if (body !== void 0 && !hasContentTypeHeader) {
195
+ normalizedHeaders["Content-Type"] = "application/json";
196
+ }
197
+ return normalizedHeaders;
198
+ }
199
+ async parseErrorResponse(response) {
200
+ const defaultError = {
201
+ message: response.statusText || "\uC694\uCCAD\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4."
202
+ };
203
+ let rawBody = "";
204
+ if (typeof response.text === "function") {
205
+ rawBody = await response.text();
206
+ } else if (typeof response.json === "function") {
207
+ try {
208
+ const parsedJson = await response.json();
209
+ if (typeof parsedJson.message === "string") {
210
+ return {
211
+ message: parsedJson.message,
212
+ errors: parsedJson.errors
213
+ };
214
+ }
215
+ } catch {
216
+ return defaultError;
217
+ }
218
+ }
219
+ if (!rawBody) {
220
+ return defaultError;
221
+ }
222
+ try {
223
+ const parsed = JSON.parse(rawBody);
224
+ if (typeof parsed.message === "string") {
225
+ return {
226
+ message: parsed.message,
227
+ errors: parsed.errors
228
+ };
229
+ }
230
+ } catch {
231
+ return {
232
+ message: rawBody
233
+ };
234
+ }
235
+ return defaultError;
236
+ }
237
+ parseRetryAfterSeconds(response) {
238
+ const retryAfterRaw = response.headers?.get?.("retry-after");
239
+ if (!retryAfterRaw) {
240
+ return void 0;
241
+ }
242
+ const parsed = Number.parseInt(retryAfterRaw, 10);
243
+ return Number.isFinite(parsed) ? parsed : void 0;
244
+ }
245
+ get(path) {
246
+ return this.request(path, { method: "GET" });
247
+ }
248
+ post(path, body) {
249
+ return this.request(path, { method: "POST", body });
250
+ }
251
+ put(path, body) {
252
+ return this.request(path, { method: "PUT", body });
253
+ }
254
+ delete(path) {
255
+ return this.request(path, { method: "DELETE" });
256
+ }
257
+ };
258
+
259
+ // src/utils/path.ts
260
+ function encodePathSegment(value, fieldName) {
261
+ if (!value) {
262
+ throw new Error(`${fieldName}\uB294 \uD544\uC218\uC785\uB2C8\uB2E4.`);
263
+ }
264
+ return encodeURIComponent(value);
265
+ }
266
+
267
+ // src/resources/projects.ts
268
+ var ProjectsResource = class {
269
+ constructor(client) {
270
+ this.client = client;
271
+ }
272
+ /**
273
+ * 프로젝트 목록 조회
274
+ *
275
+ * @example
276
+ * ```ts
277
+ * const projects = await client.projects.list();
278
+ * console.log(projects);
279
+ * ```
280
+ */
281
+ async list() {
282
+ return this.client.get("/api/v1/projects");
283
+ }
284
+ /**
285
+ * 프로젝트 상세 조회
286
+ *
287
+ * @param projectId - 프로젝트 ID
288
+ *
289
+ * @example
290
+ * ```ts
291
+ * const project = await client.projects.get("clq1234abcd");
292
+ * console.log(project.files);
293
+ * ```
294
+ */
295
+ async get(projectId) {
296
+ const encodedProjectId = encodePathSegment(projectId, "projectId");
297
+ return this.client.get(
298
+ `/api/v1/projects/${encodedProjectId}`
299
+ );
300
+ }
301
+ /**
302
+ * 프로젝트 생성
303
+ *
304
+ * @param data - 프로젝트 생성 데이터
305
+ *
306
+ * @example
307
+ * ```ts
308
+ * const project = await client.projects.create({
309
+ * name: "My Project",
310
+ * description: "Project description",
311
+ * storageProvider: "OCI"
312
+ * });
313
+ * ```
314
+ */
315
+ async create(data) {
316
+ return this.client.post("/api/v1/projects", data);
317
+ }
318
+ /**
319
+ * 프로젝트 삭제
320
+ *
321
+ * @param projectId - 프로젝트 ID
322
+ *
323
+ * @example
324
+ * ```ts
325
+ * await client.projects.delete("clq1234abcd");
326
+ * ```
327
+ */
328
+ async delete(projectId) {
329
+ const encodedProjectId = encodePathSegment(projectId, "projectId");
330
+ return this.client.delete(
331
+ `/api/v1/projects/${encodedProjectId}`
332
+ );
333
+ }
334
+ };
335
+
336
+ // src/resources/files.ts
337
+ var FilesResource = class {
338
+ constructor(client) {
339
+ this.client = client;
340
+ }
341
+ /**
342
+ * Presigned URL 생성
343
+ *
344
+ * @param projectId - 프로젝트 ID
345
+ * @param data - Presign 요청 데이터
346
+ */
347
+ async presign(projectId, data) {
348
+ const encodedProjectId = encodePathSegment(projectId, "projectId");
349
+ return this.client.post(
350
+ `/api/v1/projects/${encodedProjectId}/files/presign`,
351
+ data
352
+ );
353
+ }
354
+ /**
355
+ * 업로드 완료 확인
356
+ *
357
+ * @param projectId - 프로젝트 ID
358
+ * @param data - Confirm 요청 데이터
359
+ */
360
+ async confirm(projectId, data) {
361
+ const encodedProjectId = encodePathSegment(projectId, "projectId");
362
+ return this.client.post(
363
+ `/api/v1/projects/${encodedProjectId}/files/confirm`,
364
+ data
365
+ );
366
+ }
367
+ /**
368
+ * 파일 삭제
369
+ *
370
+ * @param projectId - 프로젝트 ID
371
+ * @param fileId - 파일 ID
372
+ *
373
+ * @example
374
+ * ```ts
375
+ * await client.files.delete("clq1234abcd", "file5678efgh");
376
+ * ```
377
+ */
378
+ async delete(projectId, fileId) {
379
+ const encodedProjectId = encodePathSegment(projectId, "projectId");
380
+ const encodedFileId = encodePathSegment(fileId, "fileId");
381
+ return this.client.delete(
382
+ `/api/v1/projects/${encodedProjectId}/files/${encodedFileId}`
383
+ );
384
+ }
385
+ /**
386
+ * 파일 업로드 (고수준 헬퍼)
387
+ *
388
+ * presign → 직접 업로드 → confirm 플로우를 자동으로 처리합니다.
389
+ *
390
+ * @param projectId - 프로젝트 ID
391
+ * @param file - 업로드할 파일
392
+ * @param options - 업로드 옵션 (이미지 변환 등)
393
+ *
394
+ * @example
395
+ * ```ts
396
+ * // 브라우저에서
397
+ * const input = document.querySelector('input[type="file"]');
398
+ * const file = input.files[0];
399
+ *
400
+ * const result = await client.files.upload("projectId", file, {
401
+ * variants: [
402
+ * { sizeLabel: "max800", format: "webp" },
403
+ * { sizeLabel: "1200x800", format: "avif" }
404
+ * ],
405
+ * onProgress: (progress) => console.log(progress)
406
+ * });
407
+ * ```
408
+ *
409
+ * @example
410
+ * ```ts
411
+ * // Node.js에서
412
+ * import { readFile } from "fs/promises";
413
+ *
414
+ * const buffer = await readFile("./image.jpg");
415
+ * const file = {
416
+ * name: "image.jpg",
417
+ * type: "image/jpeg",
418
+ * size: buffer.byteLength,
419
+ * arrayBuffer: async () => buffer
420
+ * };
421
+ *
422
+ * const result = await client.files.upload("projectId", file);
423
+ * ```
424
+ */
425
+ async upload(projectId, file, options = {}) {
426
+ const { variants, onProgress } = options;
427
+ onProgress?.({ stage: "presign" });
428
+ const presignData = {
429
+ fileName: file.name,
430
+ contentType: file.type,
431
+ fileSize: file.size
432
+ };
433
+ const presignResult = await this.presign(projectId, presignData);
434
+ onProgress?.({ stage: "upload", percent: 0 });
435
+ const fileBuffer = await file.arrayBuffer();
436
+ try {
437
+ const uploadResponse = await fetch(presignResult.presignedUrl, {
438
+ method: "PUT",
439
+ headers: {
440
+ "Content-Type": file.type
441
+ },
442
+ body: fileBuffer
443
+ });
444
+ if (!uploadResponse.ok) {
445
+ throw new NetworkError(
446
+ `\uD30C\uC77C \uC5C5\uB85C\uB4DC\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4: ${uploadResponse.statusText}`
447
+ );
448
+ }
449
+ } catch (error) {
450
+ if (error instanceof NetworkError) {
451
+ throw error;
452
+ }
453
+ throw new NetworkError(
454
+ error instanceof Error ? error.message : "\uD30C\uC77C \uC5C5\uB85C\uB4DC\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4."
455
+ );
456
+ }
457
+ onProgress?.({ stage: "upload", percent: 100 });
458
+ onProgress?.({ stage: "confirm" });
459
+ const confirmData = {
460
+ fileId: presignResult.fileId,
461
+ objectName: presignResult.objectName,
462
+ fileName: file.name,
463
+ contentType: file.type,
464
+ fileSize: file.size,
465
+ variants
466
+ };
467
+ const confirmResult = await this.confirm(projectId, confirmData);
468
+ return confirmResult.file;
469
+ }
470
+ };
471
+
472
+ // src/client.ts
473
+ var LeemageClient = class {
474
+ constructor(options) {
475
+ const apiKey = options.apiKey?.trim();
476
+ if (!apiKey) {
477
+ throw new Error("apiKey\uB294 \uD544\uC218\uC785\uB2C8\uB2E4.");
478
+ }
479
+ const baseUrl = options.baseUrl?.trim();
480
+ if (!baseUrl) {
481
+ throw new Error("baseUrl\uC740 \uD544\uC218\uC785\uB2C8\uB2E4.");
482
+ }
483
+ this.client = new FetchClient({
484
+ baseUrl,
485
+ apiKey,
486
+ timeout: options.timeout,
487
+ allowInsecureHttp: options.allowInsecureHttp
488
+ });
489
+ this.projects = new ProjectsResource(this.client);
490
+ this.files = new FilesResource(this.client);
491
+ }
492
+ };
493
+
494
+ exports.AuthenticationError = AuthenticationError;
495
+ exports.FileTooLargeError = FileTooLargeError;
496
+ exports.FilesResource = FilesResource;
497
+ exports.LeemageClient = LeemageClient;
498
+ exports.LeemageError = LeemageError;
499
+ exports.NetworkError = NetworkError;
500
+ exports.NotFoundError = NotFoundError;
501
+ exports.PermissionDeniedError = PermissionDeniedError;
502
+ exports.ProjectsResource = ProjectsResource;
503
+ exports.RateLimitError = RateLimitError;
504
+ exports.ServerError = ServerError;
505
+ exports.ValidationError = ValidationError;
506
+ //# sourceMappingURL=index.js.map
507
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors/index.ts","../src/utils/fetch.ts","../src/utils/path.ts","../src/resources/projects.ts","../src/resources/files.ts","../src/client.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { ErrorResponse } from \"../types/api\";\n\n/**\n * Leemage API 기본 에러\n */\nexport class LeemageError extends Error {\n readonly status: number;\n readonly errors?: Record<string, string[]>;\n\n constructor(\n message: string,\n status: number,\n errors?: Record<string, string[]>\n ) {\n super(message);\n this.name = \"LeemageError\";\n this.status = status;\n this.errors = errors;\n }\n\n static fromResponse(response: ErrorResponse, status: number): LeemageError {\n switch (status) {\n case 400:\n return new ValidationError(response.message, status, response.errors);\n case 401:\n return new AuthenticationError(\n response.message,\n status,\n response.errors\n );\n case 403:\n return new PermissionDeniedError(\n response.message,\n status,\n response.errors\n );\n case 404:\n return new NotFoundError(response.message, status, response.errors);\n case 429:\n return new RateLimitError(response.message, status, response.errors);\n case 413:\n return new FileTooLargeError(response.message, status, response.errors);\n case 500:\n return new ServerError(response.message, status, response.errors);\n default:\n return new LeemageError(response.message, status, response.errors);\n }\n }\n}\n\n/**\n * 인증 에러 (401)\n */\nexport class AuthenticationError extends LeemageError {\n constructor(\n message = \"인증에 실패했습니다. API 키를 확인해주세요.\",\n status = 401,\n errors?: Record<string, string[]>\n ) {\n super(message, status, errors);\n this.name = \"AuthenticationError\";\n }\n}\n\n/**\n * 리소스 없음 에러 (404)\n */\nexport class NotFoundError extends LeemageError {\n constructor(\n message = \"리소스를 찾을 수 없습니다.\",\n status = 404,\n errors?: Record<string, string[]>\n ) {\n super(message, status, errors);\n this.name = \"NotFoundError\";\n }\n}\n\n/**\n * 권한 부족 에러 (403)\n */\nexport class PermissionDeniedError extends LeemageError {\n constructor(\n message = \"요청 권한이 없습니다.\",\n status = 403,\n errors?: Record<string, string[]>\n ) {\n super(message, status, errors);\n this.name = \"PermissionDeniedError\";\n }\n}\n\n/**\n * 유효성 검사 에러 (400)\n */\nexport class ValidationError extends LeemageError {\n constructor(\n message = \"잘못된 요청입니다.\",\n status = 400,\n errors?: Record<string, string[]>\n ) {\n super(message, status, errors);\n this.name = \"ValidationError\";\n }\n}\n\n/**\n * 요청 제한 초과 에러 (429)\n */\nexport class RateLimitError extends LeemageError {\n readonly retryAfter?: number;\n\n constructor(\n message = \"요청 한도를 초과했습니다. 잠시 후 다시 시도해주세요.\",\n status = 429,\n errors?: Record<string, string[]>,\n retryAfter?: number\n ) {\n super(message, status, errors);\n this.name = \"RateLimitError\";\n this.retryAfter = retryAfter;\n }\n}\n\n/**\n * 파일 크기 초과 에러 (413)\n */\nexport class FileTooLargeError extends LeemageError {\n constructor(\n message = \"파일 크기가 제한을 초과했습니다.\",\n status = 413,\n errors?: Record<string, string[]>\n ) {\n super(message, status, errors);\n this.name = \"FileTooLargeError\";\n }\n}\n\n/**\n * 서버 에러 (500)\n */\nexport class ServerError extends LeemageError {\n constructor(\n message = \"서버 오류가 발생했습니다.\",\n status = 500,\n errors?: Record<string, string[]>\n ) {\n super(message, status, errors);\n this.name = \"ServerError\";\n }\n}\n\n/**\n * 네트워크 에러\n */\nexport class NetworkError extends LeemageError {\n constructor(message = \"네트워크 연결에 실패했습니다.\") {\n super(message, 0);\n this.name = \"NetworkError\";\n }\n}\n","import {\n LeemageError,\n NetworkError,\n PermissionDeniedError,\n RateLimitError,\n} from \"../errors\";\nimport type { ErrorResponse } from \"../types/api\";\n\nexport interface FetchClientOptions {\n baseUrl: string;\n apiKey: string;\n timeout?: number;\n allowInsecureHttp?: boolean;\n}\n\nexport interface RequestOptions {\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\";\n body?: unknown;\n headers?: Record<string, string>;\n}\n\n/**\n * HTTP 클라이언트 래퍼\n */\nexport class FetchClient {\n private readonly baseUrl: string;\n private readonly apiKey: string;\n private readonly timeout: number;\n\n constructor(options: FetchClientOptions) {\n let parsedBaseUrl: URL;\n try {\n parsedBaseUrl = new URL(options.baseUrl);\n } catch {\n throw new Error(\"baseUrl은 올바른 절대 URL이어야 합니다.\");\n }\n\n const isHttps = parsedBaseUrl.protocol === \"https:\";\n const isHttp = parsedBaseUrl.protocol === \"http:\";\n if (!isHttps && !(options.allowInsecureHttp && isHttp)) {\n throw new Error(\n \"보안을 위해 baseUrl은 https 프로토콜이어야 합니다. 로컬 개발 환경에서는 allowInsecureHttp 옵션을 사용할 수 있습니다.\"\n );\n }\n\n this.baseUrl = parsedBaseUrl.toString().replace(/\\/$/, \"\");\n this.apiKey = options.apiKey;\n this.timeout = options.timeout ?? 30000;\n\n if (!Number.isFinite(this.timeout) || this.timeout <= 0) {\n throw new Error(\"timeout은 0보다 큰 숫자여야 합니다.\");\n }\n }\n\n /**\n * API 요청 실행\n */\n async request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n const url = `${this.baseUrl}${path}`;\n const { method = \"GET\", body, headers = {} } = options;\n const normalizedHeaders = this.createRequestHeaders(body, headers);\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(url, {\n method,\n headers: normalizedHeaders,\n body: body ? JSON.stringify(body) : undefined,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n const errorData = await this.parseErrorResponse(response);\n const retryAfter = this.parseRetryAfterSeconds(response);\n\n if (response.status === 403) {\n throw new PermissionDeniedError(\n errorData.message,\n response.status,\n errorData.errors\n );\n }\n\n if (response.status === 429) {\n throw new RateLimitError(\n errorData.message,\n response.status,\n errorData.errors,\n retryAfter\n );\n }\n\n throw LeemageError.fromResponse(errorData, response.status);\n }\n\n // 204 No Content 처리\n if (response.status === 204) {\n return undefined as T;\n }\n\n const contentLength = response.headers?.get?.(\"content-length\");\n if (contentLength === \"0\") {\n return undefined as T;\n }\n\n if (typeof response.text === \"function\") {\n const responseText = await response.text();\n if (!responseText) {\n return undefined as T;\n }\n\n try {\n return JSON.parse(responseText) as T;\n } catch {\n return responseText as T;\n }\n }\n\n if (typeof response.json === \"function\") {\n return (await response.json()) as T;\n }\n\n return undefined as T;\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof LeemageError) {\n throw error;\n }\n\n if (error instanceof Error) {\n if (error.name === \"AbortError\") {\n throw new NetworkError(\"요청 시간이 초과되었습니다.\");\n }\n throw new NetworkError(error.message);\n }\n\n throw new NetworkError(\"알 수 없는 오류가 발생했습니다.\");\n }\n }\n\n private createRequestHeaders(\n body: unknown,\n headers: Record<string, string>\n ): Record<string, string> {\n const normalizedHeaders: Record<string, string> = {\n Authorization: `Bearer ${this.apiKey}`,\n ...headers,\n };\n\n const hasContentTypeHeader = Object.keys(normalizedHeaders).some(\n (key) => key.toLowerCase() === \"content-type\"\n );\n\n if (body !== undefined && !hasContentTypeHeader) {\n normalizedHeaders[\"Content-Type\"] = \"application/json\";\n }\n\n return normalizedHeaders;\n }\n\n private async parseErrorResponse(response: Response): Promise<ErrorResponse> {\n const defaultError: ErrorResponse = {\n message: response.statusText || \"요청에 실패했습니다.\",\n };\n\n let rawBody = \"\";\n if (typeof response.text === \"function\") {\n rawBody = await response.text();\n } else if (typeof response.json === \"function\") {\n try {\n const parsedJson = (await response.json()) as Partial<ErrorResponse>;\n if (typeof parsedJson.message === \"string\") {\n return {\n message: parsedJson.message,\n errors: parsedJson.errors,\n };\n }\n } catch {\n return defaultError;\n }\n }\n\n if (!rawBody) {\n return defaultError;\n }\n\n try {\n const parsed = JSON.parse(rawBody) as Partial<ErrorResponse>;\n if (typeof parsed.message === \"string\") {\n return {\n message: parsed.message,\n errors: parsed.errors,\n };\n }\n } catch {\n return {\n message: rawBody,\n };\n }\n\n return defaultError;\n }\n\n private parseRetryAfterSeconds(response: Response): number | undefined {\n const retryAfterRaw = response.headers?.get?.(\"retry-after\");\n if (!retryAfterRaw) {\n return undefined;\n }\n\n const parsed = Number.parseInt(retryAfterRaw, 10);\n return Number.isFinite(parsed) ? parsed : undefined;\n }\n\n get<T>(path: string): Promise<T> {\n return this.request<T>(path, { method: \"GET\" });\n }\n\n post<T>(path: string, body?: unknown): Promise<T> {\n return this.request<T>(path, { method: \"POST\", body });\n }\n\n put<T>(path: string, body?: unknown): Promise<T> {\n return this.request<T>(path, { method: \"PUT\", body });\n }\n\n delete<T>(path: string): Promise<T> {\n return this.request<T>(path, { method: \"DELETE\" });\n }\n}\n","/**\n * URL path segment를 안전하게 인코딩합니다.\n */\nexport function encodePathSegment(value: string, fieldName: string): string {\n if (!value) {\n throw new Error(`${fieldName}는 필수입니다.`);\n }\n\n return encodeURIComponent(value);\n}\n","import type { FetchClient } from \"../utils/fetch\";\nimport type {\n Project,\n ProjectDetails,\n CreateProjectRequest,\n MessageResponse,\n} from \"../types/api\";\nimport { encodePathSegment } from \"../utils/path\";\n\n/**\n * Projects API 클라이언트\n */\nexport class ProjectsResource {\n constructor(private readonly client: FetchClient) {}\n\n /**\n * 프로젝트 목록 조회\n *\n * @example\n * ```ts\n * const projects = await client.projects.list();\n * console.log(projects);\n * ```\n */\n async list(): Promise<Project[]> {\n return this.client.get<Project[]>(\"/api/v1/projects\");\n }\n\n /**\n * 프로젝트 상세 조회\n *\n * @param projectId - 프로젝트 ID\n *\n * @example\n * ```ts\n * const project = await client.projects.get(\"clq1234abcd\");\n * console.log(project.files);\n * ```\n */\n async get(projectId: string): Promise<ProjectDetails> {\n const encodedProjectId = encodePathSegment(projectId, \"projectId\");\n return this.client.get<ProjectDetails>(\n `/api/v1/projects/${encodedProjectId}`\n );\n }\n\n /**\n * 프로젝트 생성\n *\n * @param data - 프로젝트 생성 데이터\n *\n * @example\n * ```ts\n * const project = await client.projects.create({\n * name: \"My Project\",\n * description: \"Project description\",\n * storageProvider: \"OCI\"\n * });\n * ```\n */\n async create(data: CreateProjectRequest): Promise<Project> {\n return this.client.post<Project>(\"/api/v1/projects\", data);\n }\n\n /**\n * 프로젝트 삭제\n *\n * @param projectId - 프로젝트 ID\n *\n * @example\n * ```ts\n * await client.projects.delete(\"clq1234abcd\");\n * ```\n */\n async delete(projectId: string): Promise<MessageResponse> {\n const encodedProjectId = encodePathSegment(projectId, \"projectId\");\n return this.client.delete<MessageResponse>(\n `/api/v1/projects/${encodedProjectId}`\n );\n }\n}\n","import type { FetchClient } from \"../utils/fetch\";\nimport type {\n FileResponse,\n PresignRequest,\n PresignResponse,\n ConfirmRequest,\n ConfirmResponse,\n MessageResponse,\n UploadOptions,\n UploadableFile,\n} from \"../types/api\";\nimport { NetworkError } from \"../errors\";\nimport { encodePathSegment } from \"../utils/path\";\n\n/**\n * Files API 클라이언트\n */\nexport class FilesResource {\n constructor(private readonly client: FetchClient) {}\n\n /**\n * Presigned URL 생성\n *\n * @param projectId - 프로젝트 ID\n * @param data - Presign 요청 데이터\n */\n async presign(\n projectId: string,\n data: PresignRequest\n ): Promise<PresignResponse> {\n const encodedProjectId = encodePathSegment(projectId, \"projectId\");\n return this.client.post<PresignResponse>(\n `/api/v1/projects/${encodedProjectId}/files/presign`,\n data\n );\n }\n\n /**\n * 업로드 완료 확인\n *\n * @param projectId - 프로젝트 ID\n * @param data - Confirm 요청 데이터\n */\n async confirm(\n projectId: string,\n data: ConfirmRequest\n ): Promise<ConfirmResponse> {\n const encodedProjectId = encodePathSegment(projectId, \"projectId\");\n return this.client.post<ConfirmResponse>(\n `/api/v1/projects/${encodedProjectId}/files/confirm`,\n data\n );\n }\n\n /**\n * 파일 삭제\n *\n * @param projectId - 프로젝트 ID\n * @param fileId - 파일 ID\n *\n * @example\n * ```ts\n * await client.files.delete(\"clq1234abcd\", \"file5678efgh\");\n * ```\n */\n async delete(projectId: string, fileId: string): Promise<MessageResponse> {\n const encodedProjectId = encodePathSegment(projectId, \"projectId\");\n const encodedFileId = encodePathSegment(fileId, \"fileId\");\n return this.client.delete<MessageResponse>(\n `/api/v1/projects/${encodedProjectId}/files/${encodedFileId}`\n );\n }\n\n /**\n * 파일 업로드 (고수준 헬퍼)\n *\n * presign → 직접 업로드 → confirm 플로우를 자동으로 처리합니다.\n *\n * @param projectId - 프로젝트 ID\n * @param file - 업로드할 파일\n * @param options - 업로드 옵션 (이미지 변환 등)\n *\n * @example\n * ```ts\n * // 브라우저에서\n * const input = document.querySelector('input[type=\"file\"]');\n * const file = input.files[0];\n *\n * const result = await client.files.upload(\"projectId\", file, {\n * variants: [\n * { sizeLabel: \"max800\", format: \"webp\" },\n * { sizeLabel: \"1200x800\", format: \"avif\" }\n * ],\n * onProgress: (progress) => console.log(progress)\n * });\n * ```\n *\n * @example\n * ```ts\n * // Node.js에서\n * import { readFile } from \"fs/promises\";\n *\n * const buffer = await readFile(\"./image.jpg\");\n * const file = {\n * name: \"image.jpg\",\n * type: \"image/jpeg\",\n * size: buffer.byteLength,\n * arrayBuffer: async () => buffer\n * };\n *\n * const result = await client.files.upload(\"projectId\", file);\n * ```\n */\n async upload(\n projectId: string,\n file: UploadableFile,\n options: UploadOptions = {}\n ): Promise<FileResponse> {\n const { variants, onProgress } = options;\n\n // 1. Presign 요청\n onProgress?.({ stage: \"presign\" });\n\n const presignData: PresignRequest = {\n fileName: file.name,\n contentType: file.type,\n fileSize: file.size,\n };\n\n const presignResult = await this.presign(projectId, presignData);\n\n // 2. Presigned URL로 직접 업로드\n onProgress?.({ stage: \"upload\", percent: 0 });\n\n const fileBuffer = await file.arrayBuffer();\n\n try {\n const uploadResponse = await fetch(presignResult.presignedUrl, {\n method: \"PUT\",\n headers: {\n \"Content-Type\": file.type,\n },\n body: fileBuffer,\n });\n\n if (!uploadResponse.ok) {\n throw new NetworkError(\n `파일 업로드에 실패했습니다: ${uploadResponse.statusText}`\n );\n }\n } catch (error) {\n if (error instanceof NetworkError) {\n throw error;\n }\n throw new NetworkError(\n error instanceof Error ? error.message : \"파일 업로드에 실패했습니다.\"\n );\n }\n\n onProgress?.({ stage: \"upload\", percent: 100 });\n\n // 3. Confirm 요청\n onProgress?.({ stage: \"confirm\" });\n\n const confirmData: ConfirmRequest = {\n fileId: presignResult.fileId,\n objectName: presignResult.objectName,\n fileName: file.name,\n contentType: file.type,\n fileSize: file.size,\n variants,\n };\n\n const confirmResult = await this.confirm(projectId, confirmData);\n\n return confirmResult.file;\n }\n}\n","import { FetchClient } from \"./utils/fetch\";\nimport { ProjectsResource } from \"./resources/projects\";\nimport { FilesResource } from \"./resources/files\";\n\n/**\n * Leemage 클라이언트 옵션\n */\nexport interface LeemageClientOptions {\n /**\n * API 키\n *\n * Leemage 대시보드의 계정 설정에서 발급할 수 있습니다.\n */\n apiKey: string;\n\n /**\n * API 베이스 URL\n */\n baseUrl: string;\n\n /**\n * 요청 타임아웃 (밀리초)\n *\n * @default 30000\n */\n timeout?: number;\n\n /**\n * HTTP(baseUrl이 http://인 경우) 허용 여부\n *\n * 보안을 위해 기본값은 false이며, 로컬 개발 환경에서만 true로 설정하세요.\n * @default false\n */\n allowInsecureHttp?: boolean;\n}\n\n/**\n * Leemage API 클라이언트\n *\n * Leemage 파일 관리 플랫폼의 API를 사용하기 위한 클라이언트입니다.\n *\n * @example\n * ```ts\n * import { LeemageClient } from \"leemage-sdk\";\n *\n * const client = new LeemageClient({\n * apiKey: \"your-api-key\",\n * baseUrl: \"https://api.your-domain.com\"\n * });\n *\n * // 프로젝트 목록 조회\n * const projects = await client.projects.list();\n *\n * // 파일 업로드\n * const file = await client.files.upload(projectId, myFile, {\n * variants: [{ sizeLabel: \"max800\", format: \"webp\" }]\n * });\n * ```\n */\nexport class LeemageClient {\n /**\n * Projects API\n *\n * 프로젝트 생성, 조회, 삭제 등의 기능을 제공합니다.\n */\n readonly projects: ProjectsResource;\n\n /**\n * Files API\n *\n * 파일 업로드, 삭제 등의 기능을 제공합니다.\n */\n readonly files: FilesResource;\n\n private readonly client: FetchClient;\n\n constructor(options: LeemageClientOptions) {\n const apiKey = options.apiKey?.trim();\n if (!apiKey) {\n throw new Error(\"apiKey는 필수입니다.\");\n }\n const baseUrl = options.baseUrl?.trim();\n if (!baseUrl) {\n throw new Error(\"baseUrl은 필수입니다.\");\n }\n\n this.client = new FetchClient({\n baseUrl,\n apiKey,\n timeout: options.timeout,\n allowInsecureHttp: options.allowInsecureHttp,\n });\n\n this.projects = new ProjectsResource(this.client);\n this.files = new FilesResource(this.client);\n }\n}\n"]}