@lumir-company/editor 0.4.1 → 0.4.4
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/README.md +469 -12
- package/dist/api/link-preview.d.mts +40 -0
- package/dist/api/link-preview.d.ts +40 -0
- package/dist/api/link-preview.js +190 -0
- package/dist/api/link-preview.js.map +1 -0
- package/dist/api/link-preview.mjs +163 -0
- package/dist/api/link-preview.mjs.map +1 -0
- package/dist/index.d.mts +870 -3
- package/dist/index.d.ts +870 -3
- package/dist/index.js +2759 -49
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2749 -38
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +878 -0
- package/package.json +23 -5
package/README.md
CHANGED
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
- [S3 업로드 설정](#1-s3-업로드-권장)
|
|
18
18
|
- [파일명 커스터마이징](#파일명-커스터마이징)
|
|
19
19
|
- [커스텀 업로더](#2-커스텀-업로더)
|
|
20
|
+
- [이미지 삭제](#이미지-삭제)
|
|
21
|
+
- [HTML 미리보기](#html-미리보기)
|
|
22
|
+
- [Placeholder](#placeholder)
|
|
23
|
+
- [링크 프리뷰](#링크-프리뷰)
|
|
20
24
|
- [Props API](#props-api)
|
|
21
25
|
- [사용 예제](#사용-예제)
|
|
22
26
|
- [스타일링](#스타일링)
|
|
@@ -29,6 +33,7 @@
|
|
|
29
33
|
| 특징 | 설명 |
|
|
30
34
|
| ----------------------- | ------------------------------------------------------ |
|
|
31
35
|
| **이미지 전용** | 이미지 업로드/드래그앤드롭만 지원 (비디오/오디오 제거) |
|
|
36
|
+
| **HTML 미리보기** | HTML 파일을 드래그 앤 드롭하여 iframe으로 미리보기 |
|
|
32
37
|
| **S3 연동** | Presigned URL 기반 S3 업로드 내장 |
|
|
33
38
|
| **파일명 커스터마이징** | 업로드 파일명 변경 콜백 + UUID 자동 추가 지원 |
|
|
34
39
|
| **로딩 스피너** | 이미지 업로드 중 자동 스피너 표시 |
|
|
@@ -88,7 +93,7 @@ import "@lumir-company/editor/style.css";
|
|
|
88
93
|
const LumirEditor = dynamic(
|
|
89
94
|
() =>
|
|
90
95
|
import("@lumir-company/editor").then((m) => ({ default: m.LumirEditor })),
|
|
91
|
-
{ ssr: false }
|
|
96
|
+
{ ssr: false },
|
|
92
97
|
);
|
|
93
98
|
|
|
94
99
|
export default function EditorPage() {
|
|
@@ -329,19 +334,399 @@ const imageUrl = await s3Uploader(imageFile);
|
|
|
329
334
|
|
|
330
335
|
---
|
|
331
336
|
|
|
337
|
+
## 이미지 삭제
|
|
338
|
+
|
|
339
|
+
에디터에서 이미지가 삭제될 때 S3 등 외부 스토리지에서도 자동으로 삭제하고 싶다면 `onImageDelete` 콜백을 사용하세요.
|
|
340
|
+
|
|
341
|
+
### 기본 사용
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
<LumirEditor
|
|
345
|
+
s3Upload={{
|
|
346
|
+
apiEndpoint: "/api/s3/presigned",
|
|
347
|
+
env: "production",
|
|
348
|
+
path: "images",
|
|
349
|
+
}}
|
|
350
|
+
onImageDelete={(imageUrl) => {
|
|
351
|
+
console.log("이미지 삭제됨:", imageUrl);
|
|
352
|
+
// S3에서 삭제 로직 구현
|
|
353
|
+
}}
|
|
354
|
+
/>
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### 권장: 지연 삭제 (Undo/Redo 대응)
|
|
358
|
+
|
|
359
|
+
Undo로 이미지를 복원할 수 있도록 **지연 삭제**를 권장합니다.
|
|
360
|
+
|
|
361
|
+
```tsx
|
|
362
|
+
"use client";
|
|
363
|
+
|
|
364
|
+
import { useState, useRef, useCallback } from "react";
|
|
365
|
+
|
|
366
|
+
function Editor() {
|
|
367
|
+
const pendingDeletes = useRef(new Map());
|
|
368
|
+
|
|
369
|
+
const handleImageDelete = useCallback((imageUrl: string) => {
|
|
370
|
+
// 이미 예약된 삭제가 있으면 무시
|
|
371
|
+
if (pendingDeletes.current.has(imageUrl)) return;
|
|
372
|
+
|
|
373
|
+
// 5분 후 삭제 예약
|
|
374
|
+
const timeoutId = setTimeout(
|
|
375
|
+
async () => {
|
|
376
|
+
pendingDeletes.current.delete(imageUrl);
|
|
377
|
+
|
|
378
|
+
// S3에서 실제 삭제
|
|
379
|
+
await fetch(`/api/s3/delete?url=${encodeURIComponent(imageUrl)}`, {
|
|
380
|
+
method: "DELETE",
|
|
381
|
+
});
|
|
382
|
+
},
|
|
383
|
+
5 * 60 * 1000,
|
|
384
|
+
); // 5분
|
|
385
|
+
|
|
386
|
+
pendingDeletes.current.set(imageUrl, timeoutId);
|
|
387
|
+
}, []);
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<LumirEditor
|
|
391
|
+
s3Upload={
|
|
392
|
+
{
|
|
393
|
+
/* ... */
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
onImageDelete={handleImageDelete}
|
|
397
|
+
/>
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### S3 삭제 API 예시
|
|
403
|
+
|
|
404
|
+
> **참고**: `onImageDelete`는 **프레임워크 독립적**입니다. 아래는 각 환경별 구현 예시입니다.
|
|
405
|
+
|
|
406
|
+
#### Next.js API Route
|
|
407
|
+
|
|
408
|
+
**파일**: `app/api/s3/delete/route.ts`
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
412
|
+
import { S3Client, DeleteObjectCommand } from "@aws-sdk/client-s3";
|
|
413
|
+
|
|
414
|
+
const s3 = new S3Client({
|
|
415
|
+
region: process.env.AWS_REGION!,
|
|
416
|
+
credentials: {
|
|
417
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
418
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
export async function DELETE(req: NextRequest) {
|
|
423
|
+
const { searchParams } = new URL(req.url);
|
|
424
|
+
const imageUrl = searchParams.get("url");
|
|
425
|
+
|
|
426
|
+
if (!imageUrl) {
|
|
427
|
+
return NextResponse.json({ error: "url is required" }, { status: 400 });
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// URL에서 S3 키 추출
|
|
431
|
+
const key = extractKeyFromUrl(imageUrl);
|
|
432
|
+
|
|
433
|
+
await s3.send(
|
|
434
|
+
new DeleteObjectCommand({
|
|
435
|
+
Bucket: process.env.AWS_S3_BUCKET!,
|
|
436
|
+
Key: key,
|
|
437
|
+
}),
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
return NextResponse.json({ success: true });
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function extractKeyFromUrl(url: string): string {
|
|
444
|
+
const urlObj = new URL(url);
|
|
445
|
+
return decodeURIComponent(urlObj.pathname.slice(1));
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
**클라이언트 구현**:
|
|
450
|
+
|
|
451
|
+
```tsx
|
|
452
|
+
const handleImageDelete = (imageUrl: string) => {
|
|
453
|
+
fetch(`/api/s3/delete?url=${encodeURIComponent(imageUrl)}`, {
|
|
454
|
+
method: "DELETE",
|
|
455
|
+
});
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
<LumirEditor onImageDelete={handleImageDelete} />;
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
#### React + Express
|
|
462
|
+
|
|
463
|
+
**서버** (`server.js`):
|
|
464
|
+
|
|
465
|
+
```javascript
|
|
466
|
+
app.delete("/api/images", async (req, res) => {
|
|
467
|
+
const { imageUrl } = req.body;
|
|
468
|
+
const key = extractKeyFromS3Url(imageUrl);
|
|
469
|
+
|
|
470
|
+
await s3Client.send(
|
|
471
|
+
new DeleteObjectCommand({
|
|
472
|
+
Bucket: process.env.S3_BUCKET,
|
|
473
|
+
Key: key,
|
|
474
|
+
}),
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
res.json({ success: true });
|
|
478
|
+
});
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**클라이언트**:
|
|
482
|
+
|
|
483
|
+
```tsx
|
|
484
|
+
const handleImageDelete = async (imageUrl: string) => {
|
|
485
|
+
await fetch("https://api.myapp.com/api/images", {
|
|
486
|
+
method: "DELETE",
|
|
487
|
+
headers: { "Content-Type": "application/json" },
|
|
488
|
+
body: JSON.stringify({ imageUrl }),
|
|
489
|
+
});
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
<LumirEditor onImageDelete={handleImageDelete} />;
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
#### React Native + Firebase Storage
|
|
496
|
+
|
|
497
|
+
```tsx
|
|
498
|
+
import storage from "@react-native-firebase/storage";
|
|
499
|
+
|
|
500
|
+
const handleImageDelete = async (imageUrl: string) => {
|
|
501
|
+
const ref = storage().refFromURL(imageUrl);
|
|
502
|
+
await ref.delete();
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
<LumirEditor onImageDelete={handleImageDelete} />;
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
#### Vue + Axios + FastAPI
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
const handleImageDelete = async (imageUrl: string) => {
|
|
512
|
+
await axios.delete("https://api.myapp.com/v1/images", {
|
|
513
|
+
data: { imageUrl },
|
|
514
|
+
});
|
|
515
|
+
};
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### 주의사항
|
|
519
|
+
|
|
520
|
+
| 항목 | 설명 |
|
|
521
|
+
| --------------- | --------------------------------------------- |
|
|
522
|
+
| **Undo/Redo** | 지연 삭제로 복원 가능하게 구현 (권장: 5-10분) |
|
|
523
|
+
| **권한 검증** | 프로덕션에서는 인증/인가 필수 |
|
|
524
|
+
| **참조 카운트** | 같은 이미지를 여러 문서에서 사용하는지 확인 |
|
|
525
|
+
| **삭제 로그** | 감사 추적을 위한 삭제 기록 저장 권장 |
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## HTML 미리보기
|
|
530
|
+
|
|
531
|
+
LumirEditor는 HTML 파일을 iframe을 사용하여 미리보기할 수 있는 커스텀 블록을 제공합니다. 편집 불가능한 순수 미리보기 기능으로, HTML 문서를 안전하게 표시할 수 있습니다.
|
|
532
|
+
|
|
533
|
+
### 사용 방법
|
|
534
|
+
|
|
535
|
+
#### 1. 드래그 앤 드롭
|
|
536
|
+
|
|
537
|
+
HTML 파일(`.html`, `.htm`)을 에디터에 드래그 앤 드롭하면 자동으로 iframe 미리보기 블록이 삽입됩니다.
|
|
538
|
+
|
|
539
|
+
```tsx
|
|
540
|
+
<LumirEditor />
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
- **지원 파일 형식**: `.html`, `.htm`
|
|
544
|
+
- **특징**:
|
|
545
|
+
- 편집 불가능한 순수 미리보기
|
|
546
|
+
- 접기/펼치기 기능
|
|
547
|
+
- 안전한 sandbox 처리 (`allow-same-origin`, JavaScript 실행 비활성화)
|
|
548
|
+
- 파일명 표시
|
|
549
|
+
|
|
550
|
+
#### 2. 슬래시 메뉴
|
|
551
|
+
|
|
552
|
+
에디터에서 `/`를 입력하고 "HTML Preview"를 선택하면 예제 HTML 미리보기 블록이 삽입됩니다.
|
|
553
|
+
|
|
554
|
+
```
|
|
555
|
+
/ → HTML Preview
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### 특징
|
|
559
|
+
|
|
560
|
+
- **iframe 기반**: HTML 문서를 독립된 iframe에서 안전하게 렌더링
|
|
561
|
+
- **Sandbox 보안**: `sandbox="allow-same-origin"` 속성으로 보안 강화 (JavaScript 실행 의도적 비활성화)
|
|
562
|
+
- **접기/펼치기**: 헤더 클릭으로 미리보기 영역 토글
|
|
563
|
+
- **드래그 리사이즈**: 하단 핸들을 드래그하여 높이 조절 가능 (100px ~ 1200px)
|
|
564
|
+
- **새 창 열기**: HTML 문서를 새 창에서 전체 화면으로 확인
|
|
565
|
+
- **다운로드**: HTML 파일로 다운로드
|
|
566
|
+
- **편집 불가**: 순수 미리보기 전용
|
|
567
|
+
|
|
568
|
+
### 사용 예제
|
|
569
|
+
|
|
570
|
+
```tsx
|
|
571
|
+
import { LumirEditor } from "@lumir-company/editor";
|
|
572
|
+
import "@lumir-company/editor/style.css";
|
|
573
|
+
|
|
574
|
+
function App() {
|
|
575
|
+
return (
|
|
576
|
+
<div className="w-full h-[600px]">
|
|
577
|
+
<LumirEditor
|
|
578
|
+
onContentChange={(blocks) => {
|
|
579
|
+
// HTML 미리보기 블록도 일반 블록과 동일하게 처리됨
|
|
580
|
+
console.log(blocks);
|
|
581
|
+
}}
|
|
582
|
+
/>
|
|
583
|
+
</div>
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### 프로그래밍 방식으로 블록 삽입
|
|
589
|
+
|
|
590
|
+
```tsx
|
|
591
|
+
import { HtmlPreview } from "@lumir-company/editor";
|
|
592
|
+
|
|
593
|
+
// 에디터 인스턴스에서 직접 블록 삽입
|
|
594
|
+
editor.insertBlocks([
|
|
595
|
+
{
|
|
596
|
+
type: "htmlPreview",
|
|
597
|
+
props: {
|
|
598
|
+
htmlContent: "<h1>Hello World</h1><p>This is HTML content</p>",
|
|
599
|
+
fileName: "example.html",
|
|
600
|
+
height: "400px",
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
]);
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### 주의사항
|
|
607
|
+
|
|
608
|
+
- HTML 내용은 iframe의 `sandbox="allow-same-origin"` 속성으로 보안이 강화되어 있습니다
|
|
609
|
+
- **JavaScript는 의도적으로 비활성화**되어 있습니다 (보안상 이유)
|
|
610
|
+
- 외부 리소스(CSS, JS, 이미지 등)는 상대 경로가 작동하지 않을 수 있습니다
|
|
611
|
+
- 인라인 CSS 스타일을 권장합니다
|
|
612
|
+
|
|
613
|
+
---
|
|
614
|
+
|
|
615
|
+
## Placeholder
|
|
616
|
+
|
|
617
|
+
에디터가 비어있을 때 사용자에게 안내 텍스트를 표시합니다.
|
|
618
|
+
|
|
619
|
+
### 사용 방법
|
|
620
|
+
|
|
621
|
+
```tsx
|
|
622
|
+
<LumirEditor placeholder="내용을 입력하세요..." />
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
- 빈 블록에 연한 색상으로 안내 텍스트가 표시됩니다
|
|
626
|
+
- 사용자가 입력을 시작하면 자동으로 사라집니다
|
|
627
|
+
- 모든 빈 블록(첫 번째 블록 포함)에 동일한 텍스트가 적용됩니다
|
|
628
|
+
|
|
629
|
+
---
|
|
630
|
+
|
|
631
|
+
## 링크 프리뷰
|
|
632
|
+
|
|
633
|
+
URL을 붙여넣거나 슬래시 메뉴에서 선택하면 Open Graph 카드를 표시합니다.
|
|
634
|
+
|
|
635
|
+
> **서버 사이드 필수**: 링크 프리뷰는 외부 사이트의 OG 메타데이터를 가져오기 위해 **서버 사이드 API 라우트**가 필요합니다. 브라우저의 CORS 정책으로 인해 클라이언트에서 직접 외부 HTML을 가져올 수 없습니다.
|
|
636
|
+
|
|
637
|
+
### 사용 조건
|
|
638
|
+
|
|
639
|
+
| 조건 | 설명 |
|
|
640
|
+
| -------------------------- | -------------------------------------------------------------------------- |
|
|
641
|
+
| **서버 환경** | Next.js, Remix, SvelteKit 등 서버 사이드 라우팅을 지원하는 프레임워크 필요 |
|
|
642
|
+
| **API 라우트** | 패키지 내장 핸들러를 re-export하는 1줄짜리 파일 필요 |
|
|
643
|
+
| **순수 React (CRA, Vite)** | 별도 백엔드 서버 없이는 사용 불가 |
|
|
644
|
+
|
|
645
|
+
### 설정 방법 (Next.js App Router)
|
|
646
|
+
|
|
647
|
+
**1단계: API 라우트 생성** (1줄)
|
|
648
|
+
|
|
649
|
+
```ts
|
|
650
|
+
// src/app/api/link-preview/route.ts
|
|
651
|
+
export { linkPreviewHandler as GET } from "@lumir-company/editor/api/link-preview";
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
패키지에 내장된 핸들러를 re-export하므로 별도 로직 작성이 불필요합니다.
|
|
655
|
+
|
|
656
|
+
**2단계: 에디터에 linkPreview prop 설정**
|
|
657
|
+
|
|
658
|
+
```tsx
|
|
659
|
+
<LumirEditor linkPreview={{ apiEndpoint: "/api/link-preview" }} />
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
### 설정 방법 (Remix / SvelteKit)
|
|
663
|
+
|
|
664
|
+
패키지의 `GET` 핸들러는 표준 Web API(`Request`/`Response`)를 사용하므로 Remix, SvelteKit 등에서도 동일하게 사용 가능합니다.
|
|
665
|
+
|
|
666
|
+
```ts
|
|
667
|
+
// Remix: app/routes/api.link-preview.ts
|
|
668
|
+
export { linkPreviewHandler as loader } from "@lumir-company/editor/api/link-preview";
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
### 설정 방법 (Express / Fastify 등 커스텀 서버)
|
|
672
|
+
|
|
673
|
+
패키지에서 `fetchUrlMetadata`와 `parseMetaTags`를 import하여 직접 라우트를 구현할 수 있습니다.
|
|
674
|
+
|
|
675
|
+
```ts
|
|
676
|
+
import { fetchUrlMetadata } from "@lumir-company/editor/api/link-preview";
|
|
677
|
+
|
|
678
|
+
app.get("/api/link-preview", async (req, res) => {
|
|
679
|
+
const url = req.query.url as string;
|
|
680
|
+
if (!url) return res.status(400).json({ error: "url required" });
|
|
681
|
+
|
|
682
|
+
try {
|
|
683
|
+
const metadata = await fetchUrlMetadata(url);
|
|
684
|
+
res.json(metadata);
|
|
685
|
+
} catch {
|
|
686
|
+
res.status(500).json({ error: "Failed to fetch metadata" });
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
### 주요 기능
|
|
692
|
+
|
|
693
|
+
- URL 붙여넣기 시 자동 링크 프리뷰 블록 생성
|
|
694
|
+
- 슬래시 메뉴(`/`)에서 Link Preview 항목 선택
|
|
695
|
+
- 드래그 리사이즈 (좌우 너비, 하단 이미지 높이)
|
|
696
|
+
- 텍스트 링크를 링크 프리뷰로 전환 (링크 툴바 버튼)
|
|
697
|
+
- 메타데이터에 이미지 없으면 이미지 영역 자동 생략
|
|
698
|
+
- 에러 카드 클릭 시 링크 이동
|
|
699
|
+
|
|
700
|
+
### 내장 API 핸들러 export 목록
|
|
701
|
+
|
|
702
|
+
`@lumir-company/editor/api/link-preview`에서 export되는 항목:
|
|
703
|
+
|
|
704
|
+
| Export | 타입 | 설명 |
|
|
705
|
+
| ------------------ | ------------------------------------------------- | ------------------------------------- |
|
|
706
|
+
| `linkPreviewHandler` | `(request: Request) => Promise<Response>` | 링크 프리뷰 메타데이터 조회 핸들러 (re-export용) |
|
|
707
|
+
| `fetchUrlMetadata` | `(url: string) => Promise<LinkMetadata>` | 서버에서 직접 메타데이터 조회 |
|
|
708
|
+
| `parseMetaTags` | `(html: string, baseUrl: string) => LinkMetadata` | HTML에서 OG 메타데이터 파싱 |
|
|
709
|
+
| `LinkMetadata` | `interface` | 메타데이터 타입 정의 |
|
|
710
|
+
|
|
711
|
+
---
|
|
712
|
+
|
|
332
713
|
## Props API
|
|
333
714
|
|
|
334
715
|
### 핵심 Props
|
|
335
716
|
|
|
336
|
-
| Prop | 타입
|
|
337
|
-
| ----------------- |
|
|
338
|
-
| `s3Upload` | `S3UploaderConfig`
|
|
339
|
-
| `uploadFile` | `(file: File) => Promise<string>`
|
|
340
|
-
| `onContentChange` | `(blocks) => void`
|
|
341
|
-
| `
|
|
342
|
-
| `
|
|
343
|
-
| `
|
|
344
|
-
| `
|
|
717
|
+
| Prop | 타입 | 기본값 | 설명 |
|
|
718
|
+
| ----------------- | ----------------------------------- | ----------- | ------------------- |
|
|
719
|
+
| `s3Upload` | `S3UploaderConfig` | `undefined` | S3 업로드 설정 |
|
|
720
|
+
| `uploadFile` | `(file: File) => Promise<string>` | `undefined` | 커스텀 업로드 함수 |
|
|
721
|
+
| `onContentChange` | `(blocks) => void` | `undefined` | 콘텐츠 변경 콜백 |
|
|
722
|
+
| `onImageDelete` | `(imageUrl: string) => void` | `undefined` | 이미지 삭제 시 콜백 |
|
|
723
|
+
| `onError` | `(error: LumirEditorError) => void` | `undefined` | 에러 발생 시 콜백 |
|
|
724
|
+
| `initialContent` | `Block[] \| string` | `undefined` | 초기 콘텐츠 |
|
|
725
|
+
| `editable` | `boolean` | `true` | 편집 가능 여부 |
|
|
726
|
+
| `placeholder` | `string` | `undefined` | 빈 블록 안내 텍스트 |
|
|
727
|
+
| `linkPreview` | `{ apiEndpoint: string }` | `undefined` | 링크 프리뷰 설정 |
|
|
728
|
+
| `theme` | `"light" \| "dark"` | `"light"` | 테마 |
|
|
729
|
+
| `className` | `string` | `""` | CSS 클래스 |
|
|
345
730
|
|
|
346
731
|
### S3UploaderConfig
|
|
347
732
|
|
|
@@ -369,6 +754,7 @@ interface LumirEditorProps {
|
|
|
369
754
|
// === 에디터 설정 ===
|
|
370
755
|
initialContent?: DefaultPartialBlock[] | string; // 초기 콘텐츠 (블록 배열 또는 JSON 문자열)
|
|
371
756
|
initialEmptyBlocks?: number; // 초기 빈 블록 개수 (기본: 3)
|
|
757
|
+
placeholder?: string; // 빈 블록에 표시할 안내 텍스트 (예: "내용을 입력하세요...")
|
|
372
758
|
uploadFile?: (file: File) => Promise<string>; // 커스텀 파일 업로드 함수
|
|
373
759
|
s3Upload?: {
|
|
374
760
|
apiEndpoint: string;
|
|
@@ -381,7 +767,9 @@ interface LumirEditorProps {
|
|
|
381
767
|
|
|
382
768
|
// === 콜백 ===
|
|
383
769
|
onContentChange?: (blocks: DefaultPartialBlock[]) => void; // 콘텐츠 변경 시 호출
|
|
770
|
+
onImageDelete?: (imageUrl: string) => void; // 이미지 삭제 시 호출 (S3 삭제 등)
|
|
384
771
|
onSelectionChange?: () => void; // 선택 영역 변경 시 호출
|
|
772
|
+
onError?: (error: LumirEditorError) => void; // 에러 발생 시 호출
|
|
385
773
|
|
|
386
774
|
// 기능 설정
|
|
387
775
|
tables?: TableConfig; // 테이블 기능 설정 (splitCells, cellBackgroundColor 등)
|
|
@@ -403,6 +791,11 @@ interface LumirEditorProps {
|
|
|
403
791
|
tableHandles?: boolean; // 테이블 핸들 표시 (기본: true)
|
|
404
792
|
className?: string; // 컨테이너 CSS 클래스
|
|
405
793
|
|
|
794
|
+
// === 링크 프리뷰 설정 ===
|
|
795
|
+
linkPreview?: {
|
|
796
|
+
apiEndpoint: string; // 링크 메타데이터를 가져올 API 엔드포인트 (예: "/api/link-preview")
|
|
797
|
+
};
|
|
798
|
+
|
|
406
799
|
// 미디어 업로드 허용 여부 (기본: 모두 비활성)
|
|
407
800
|
allowVideoUpload?: boolean; // 비디오 업로드 허용 (기본: false)
|
|
408
801
|
allowAudioUpload?: boolean; // 오디오 업로드 허용 (기본: false)
|
|
@@ -475,7 +868,7 @@ import { LumirEditor, cn } from "@lumir-company/editor";
|
|
|
475
868
|
className={cn(
|
|
476
869
|
"min-h-[400px] rounded-xl",
|
|
477
870
|
"border border-gray-200 shadow-lg",
|
|
478
|
-
"focus-within:ring-2 focus-within:ring-blue-500"
|
|
871
|
+
"focus-within:ring-2 focus-within:ring-blue-500",
|
|
479
872
|
)}
|
|
480
873
|
/>;
|
|
481
874
|
```
|
|
@@ -536,7 +929,7 @@ import { LumirEditor } from "@lumir-company/editor";
|
|
|
536
929
|
const LumirEditor = dynamic(
|
|
537
930
|
() =>
|
|
538
931
|
import("@lumir-company/editor").then((m) => ({ default: m.LumirEditor })),
|
|
539
|
-
{ ssr: false }
|
|
932
|
+
{ ssr: false },
|
|
540
933
|
);
|
|
541
934
|
```
|
|
542
935
|
|
|
@@ -611,6 +1004,70 @@ const url = await uploader(imageFile);
|
|
|
611
1004
|
|
|
612
1005
|
## 변경 로그
|
|
613
1006
|
|
|
1007
|
+
### v0.4.4
|
|
1008
|
+
|
|
1009
|
+
- **Link Preview API 핸들러 내장**
|
|
1010
|
+
- `@lumir-company/editor/api/link-preview` 서브패스 export 추가
|
|
1011
|
+
- 표준 Web API (`Request`/`Response`) 기반으로 Next.js, Remix, SvelteKit 호환
|
|
1012
|
+
- 소비자는 1줄 re-export만으로 API 라우트 설정 가능 (220줄 route.ts 제거)
|
|
1013
|
+
- `linkPreviewHandler`, `fetchUrlMetadata`, `parseMetaTags` 함수 export
|
|
1014
|
+
- **Placeholder 문서화**
|
|
1015
|
+
- Placeholder 사용 가이드 추가
|
|
1016
|
+
- **README 개선**
|
|
1017
|
+
- 링크 프리뷰 서버사이드 요구사항 및 설정 방법 가이드 추가
|
|
1018
|
+
- 내장 API 핸들러 export 목록 문서화
|
|
1019
|
+
- Props API 테이블에 `placeholder`, `linkPreview` 추가
|
|
1020
|
+
|
|
1021
|
+
### v0.4.3
|
|
1022
|
+
|
|
1023
|
+
- **링크 프리뷰 기능 추가**
|
|
1024
|
+
- `linkPreview` prop으로 링크 미리보기 활성화 (카카오톡 스타일 OG 카드)
|
|
1025
|
+
- URL 붙여넣기 시 자동 링크 프리뷰 블록 생성 (빈 블록이면 교체, 텍스트 있으면 하단 삽입)
|
|
1026
|
+
- 슬래시 메뉴(`/`)에서 Link Preview 항목 추가
|
|
1027
|
+
- 드래그 리사이즈 지원 (좌우 너비, 하단 이미지 높이 조절)
|
|
1028
|
+
- 메타데이터에 이미지 없을 경우 이미지 영역 생략
|
|
1029
|
+
- 에러 카드 클릭 시 링크 이동 지원
|
|
1030
|
+
- `fetchLinkMetadata`, `clearMetadataCache`, `LinkMetadata` 타입 export
|
|
1031
|
+
- **링크 툴바 커스텀**
|
|
1032
|
+
- 텍스트 링크를 링크 프리뷰 블록으로 전환하는 버튼 추가 (`replaceBlocks` 사용)
|
|
1033
|
+
- `linkPreview.apiEndpoint` 설정 시에만 전환 버튼 표시
|
|
1034
|
+
- **placeholder prop 추가**
|
|
1035
|
+
- `placeholder` prop으로 에디터 빈 블록 안내 텍스트 설정
|
|
1036
|
+
- **이미지 삭제 기능 추가**
|
|
1037
|
+
- `onImageDelete` 콜백 prop 추가 - 에디터에서 이미지 삭제 시 호출
|
|
1038
|
+
- S3 등 외부 스토리지에서 이미지 자동 삭제 지원
|
|
1039
|
+
- 지연 삭제 패턴으로 Undo/Redo 대응 가능
|
|
1040
|
+
- 이미지 URL 추출 및 삭제 감지 헬퍼 함수 내장
|
|
1041
|
+
- **보안 강화**
|
|
1042
|
+
- URL 이스케이프 처리 추가 (XSS 방지)
|
|
1043
|
+
- LinkButton: `javascript:`, `data:`, `vbscript:`, `file:` 프로토콜 차단
|
|
1044
|
+
- 위험한 URL 입력 시 에러 메시지 표시
|
|
1045
|
+
- **링크 삽입 버그 수정**
|
|
1046
|
+
- 플로팅 메뉴 링크 버튼: 텍스트 미선택 시에도 URL 텍스트로 링크 삽입 지원
|
|
1047
|
+
- `editor.focus()` 호출로 선택 상태 복원
|
|
1048
|
+
- **README 개선**
|
|
1049
|
+
- 링크 프리뷰 사용 가이드 및 API 라우트 예시 추가
|
|
1050
|
+
- 이미지 삭제 섹션 추가 (지연 삭제 예시 포함)
|
|
1051
|
+
- S3 삭제 API 구현 예시 추가
|
|
1052
|
+
- Props API 문서 업데이트
|
|
1053
|
+
|
|
1054
|
+
### v0.4.2
|
|
1055
|
+
|
|
1056
|
+
- **코드 구조 리팩토링**
|
|
1057
|
+
- FloatingMenu 컴포넌트 분리 (Icons, 개별 버튼 컴포넌트)
|
|
1058
|
+
- 색상 상수 별도 파일로 분리 (`constants/colors.ts`)
|
|
1059
|
+
- 미사용 기능 제거 (FontSelect, FontSizeControl)
|
|
1060
|
+
- **에러 처리 개선**
|
|
1061
|
+
- `LumirEditorError` 커스텀 에러 클래스 추가
|
|
1062
|
+
- `onError` 콜백 prop 추가 - 에러 발생 시 사용자 정의 핸들링 가능
|
|
1063
|
+
- 에러 발생 시 사용자 친화적 토스트 메시지 자동 표시
|
|
1064
|
+
- **HTML 미리보기 개선**
|
|
1065
|
+
- sandbox 설정 명확화 (JavaScript 의도적 비활성화)
|
|
1066
|
+
- 드래그 리사이즈, 새 창 열기, 다운로드 기능 문서화
|
|
1067
|
+
- **타입 개선**
|
|
1068
|
+
- `LumirErrorCode`, `LumirErrorDetails` 타입 export
|
|
1069
|
+
- `ColorItem` 타입 export
|
|
1070
|
+
|
|
614
1071
|
### v0.4.1
|
|
615
1072
|
|
|
616
1073
|
- `preserveExtension` prop 추가 - 확장자 자동 붙이기 제어 (기본: true)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lumir-company/editor - Link Preview API Handler
|
|
3
|
+
*
|
|
4
|
+
* 서버 사이드 전용 모듈입니다. Next.js App Router, Remix, SvelteKit 등
|
|
5
|
+
* Web API 표준(Request/Response)을 지원하는 프레임워크에서 사용할 수 있습니다.
|
|
6
|
+
*
|
|
7
|
+
* Next.js App Router 사용 예시:
|
|
8
|
+
* ```ts
|
|
9
|
+
* // src/app/api/link-preview/route.ts
|
|
10
|
+
* export { GET } from "@lumir-company/editor/api/link-preview";
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
interface LinkMetadata {
|
|
14
|
+
url: string;
|
|
15
|
+
title: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
image?: string;
|
|
18
|
+
domain: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* HTML 문자열에서 Open Graph / Twitter Card 메타데이터를 파싱합니다.
|
|
22
|
+
* 커스텀 서버 구현 시 직접 사용할 수 있습니다.
|
|
23
|
+
*/
|
|
24
|
+
declare function parseMetaTags(html: string, baseUrl: string): LinkMetadata;
|
|
25
|
+
/**
|
|
26
|
+
* URL에서 메타데이터를 가져옵니다 (서버 사이드 전용).
|
|
27
|
+
* Express, Fastify 등 커스텀 서버에서 직접 사용할 수 있습니다.
|
|
28
|
+
*/
|
|
29
|
+
declare function fetchUrlMetadata(url: string): Promise<LinkMetadata>;
|
|
30
|
+
/**
|
|
31
|
+
* 링크 프리뷰 메타데이터 조회 핸들러 (Web API 표준 Request/Response).
|
|
32
|
+
* Next.js App Router, Remix, SvelteKit 등에서 re-export하여 사용합니다.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // Next.js: src/app/api/link-preview/route.ts
|
|
36
|
+
* export { linkPreviewHandler as GET } from "@lumir-company/editor/api/link-preview";
|
|
37
|
+
*/
|
|
38
|
+
declare function linkPreviewHandler(request: Request): Promise<Response>;
|
|
39
|
+
|
|
40
|
+
export { type LinkMetadata, fetchUrlMetadata, linkPreviewHandler, parseMetaTags };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lumir-company/editor - Link Preview API Handler
|
|
3
|
+
*
|
|
4
|
+
* 서버 사이드 전용 모듈입니다. Next.js App Router, Remix, SvelteKit 등
|
|
5
|
+
* Web API 표준(Request/Response)을 지원하는 프레임워크에서 사용할 수 있습니다.
|
|
6
|
+
*
|
|
7
|
+
* Next.js App Router 사용 예시:
|
|
8
|
+
* ```ts
|
|
9
|
+
* // src/app/api/link-preview/route.ts
|
|
10
|
+
* export { GET } from "@lumir-company/editor/api/link-preview";
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
interface LinkMetadata {
|
|
14
|
+
url: string;
|
|
15
|
+
title: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
image?: string;
|
|
18
|
+
domain: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* HTML 문자열에서 Open Graph / Twitter Card 메타데이터를 파싱합니다.
|
|
22
|
+
* 커스텀 서버 구현 시 직접 사용할 수 있습니다.
|
|
23
|
+
*/
|
|
24
|
+
declare function parseMetaTags(html: string, baseUrl: string): LinkMetadata;
|
|
25
|
+
/**
|
|
26
|
+
* URL에서 메타데이터를 가져옵니다 (서버 사이드 전용).
|
|
27
|
+
* Express, Fastify 등 커스텀 서버에서 직접 사용할 수 있습니다.
|
|
28
|
+
*/
|
|
29
|
+
declare function fetchUrlMetadata(url: string): Promise<LinkMetadata>;
|
|
30
|
+
/**
|
|
31
|
+
* 링크 프리뷰 메타데이터 조회 핸들러 (Web API 표준 Request/Response).
|
|
32
|
+
* Next.js App Router, Remix, SvelteKit 등에서 re-export하여 사용합니다.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // Next.js: src/app/api/link-preview/route.ts
|
|
36
|
+
* export { linkPreviewHandler as GET } from "@lumir-company/editor/api/link-preview";
|
|
37
|
+
*/
|
|
38
|
+
declare function linkPreviewHandler(request: Request): Promise<Response>;
|
|
39
|
+
|
|
40
|
+
export { type LinkMetadata, fetchUrlMetadata, linkPreviewHandler, parseMetaTags };
|