podo-ui 0.3.8 → 0.3.9

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.
@@ -0,0 +1,2224 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef, useEffect, useState, useCallback } from 'react';
3
+ import { v4 as uuid } from 'uuid';
4
+ import { z } from 'zod';
5
+ import styles from './editor.module.scss';
6
+ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHeight, resizable = false, onChange, validator, placeholder = '내용을 입력하세요...', }) => {
7
+ const [message, setMessage] = useState('');
8
+ const [statusClass, setStatusClass] = useState('');
9
+ const [currentParagraphStyle, setCurrentParagraphStyle] = useState('p');
10
+ const [isParagraphDropdownOpen, setIsParagraphDropdownOpen] = useState(false);
11
+ const [isTextColorOpen, setIsTextColorOpen] = useState(false);
12
+ const [isBgColorOpen, setIsBgColorOpen] = useState(false);
13
+ const [isAlignDropdownOpen, setIsAlignDropdownOpen] = useState(false);
14
+ const [currentAlign, setCurrentAlign] = useState('left');
15
+ const [isLinkDropdownOpen, setIsLinkDropdownOpen] = useState(false);
16
+ const [linkUrl, setLinkUrl] = useState('');
17
+ const [linkTarget, setLinkTarget] = useState('_blank');
18
+ const [isEditLinkPopupOpen, setIsEditLinkPopupOpen] = useState(false);
19
+ const [selectedLinkElement, setSelectedLinkElement] = useState(null);
20
+ const [editLinkUrl, setEditLinkUrl] = useState('');
21
+ const [editLinkTarget, setEditLinkTarget] = useState('_self');
22
+ const [savedSelection, setSavedSelection] = useState(null);
23
+ const [isImageDropdownOpen, setIsImageDropdownOpen] = useState(false);
24
+ const [imageTabMode, setImageTabMode] = useState('file'); // 탭 모드 추가
25
+ const [imageUrl, setImageUrl] = useState('');
26
+ const [imageWidth, setImageWidth] = useState('original'); // 기본값을 원본으로 변경
27
+ const [imageAlign, setImageAlign] = useState('left'); // 기본값을 좌측으로 변경
28
+ const [imageAlt, setImageAlt] = useState('');
29
+ const [imageFile, setImageFile] = useState(null);
30
+ const [imagePreview, setImagePreview] = useState('');
31
+ const [savedImageSelection, setSavedImageSelection] = useState(null); // 이미지 삽입용 선택 영역 저장
32
+ const [selectedImage, setSelectedImage] = useState(null); // 선택된 이미지
33
+ const [isImageEditPopupOpen, setIsImageEditPopupOpen] = useState(false); // 이미지 편집 팝업 상태
34
+ const [editImageWidth, setEditImageWidth] = useState(''); // 편집 중인 이미지 크기
35
+ const [editImageAlign, setEditImageAlign] = useState('left'); // 편집 중인 이미지 정렬
36
+ const [editImageAlt, setEditImageAlt] = useState(''); // 편집 중인 이미지 대체 텍스트
37
+ const [isResizing, setIsResizing] = useState(false); // 리사이즈 중 여부
38
+ const [resizeStartData, setResizeStartData] = useState(null);
39
+ // 유튜브 관련 상태
40
+ const [isYoutubeDropdownOpen, setIsYoutubeDropdownOpen] = useState(false);
41
+ const [youtubeUrl, setYoutubeUrl] = useState('');
42
+ const [savedYoutubeSelection, setSavedYoutubeSelection] = useState(null);
43
+ const [youtubeWidth, setYoutubeWidth] = useState('100%'); // 기본값 100%
44
+ const [youtubeAlign, setYoutubeAlign] = useState('center'); // 기본값 가운데
45
+ const [selectedYoutube, setSelectedYoutube] = useState(null); // 선택된 유튜브
46
+ const [isYoutubeEditPopupOpen, setIsYoutubeEditPopupOpen] = useState(false); // 유튜브 편집 팝업
47
+ const [editYoutubeWidth, setEditYoutubeWidth] = useState('100%'); // 편집 중인 유튜브 크기
48
+ const [editYoutubeAlign, setEditYoutubeAlign] = useState('center'); // 편집 중인 유튜브 정렬
49
+ const [isCodeView, setIsCodeView] = useState(false); // 코드보기 모드
50
+ const [codeContent, setCodeContent] = useState(''); // 코드보기 내용
51
+ const [savedEditorHeight, setSavedEditorHeight] = useState(null); // 위지윅 에디터 높이 저장
52
+ // Undo/Redo 히스토리 관리
53
+ const [history, setHistory] = useState([value]);
54
+ const [historyIndex, setHistoryIndex] = useState(0);
55
+ const historyTimerRef = useRef(null);
56
+ const historyRef = useRef([value]);
57
+ const historyIndexRef = useRef(0);
58
+ const isUndoRedoRef = useRef(false); // undo/redo 실행 중 플래그
59
+ const editorRef = useRef(null);
60
+ const codeEditorRef = useRef(null);
61
+ const containerRef = useRef(null);
62
+ const fileInputRef = useRef(null);
63
+ const paragraphButtonRef = useRef(null);
64
+ const textColorButtonRef = useRef(null);
65
+ const bgColorButtonRef = useRef(null);
66
+ const alignButtonRef = useRef(null);
67
+ const linkButtonRef = useRef(null);
68
+ const imageButtonRef = useRef(null);
69
+ const youtubeButtonRef = useRef(null);
70
+ const imageFileInputRef = useRef(null);
71
+ // 클라이언트에서만 ID 생성 (Vite React용)
72
+ const [editorID, setEditorID] = useState('podo-editor');
73
+ // 색상 팔레트 정의 (이미지 기반)
74
+ const colorPalette = [
75
+ // 첫 번째 줄: 순수 색상
76
+ ['#ff0000', '#ff8000', '#ffff00', '#80ff00', '#00ffff', '#0080ff', '#0000ff', '#8000ff', '#ff00ff', '#000000'],
77
+ // 두 번째 줄: 매우 밝은 톤 (90% 밝기)
78
+ ['#ffcccc', '#ffe0cc', '#ffffcc', '#e0ffcc', '#ccffff', '#cce0ff', '#ccccff', '#e0ccff', '#ffccff', '#cccccc'],
79
+ // 세 번째 줄: 밝은 톤 (70% 밝기)
80
+ ['#ff9999', '#ffcc99', '#ffff99', '#ccff99', '#99ffff', '#99ccff', '#9999ff', '#cc99ff', '#ff99ff', '#999999'],
81
+ // 네 번째 줄: 중간 톤 (50% 밝기)
82
+ ['#ff6666', '#ffb366', '#ffff66', '#b3ff66', '#66ffff', '#66b3ff', '#6666ff', '#b366ff', '#ff66ff', '#666666'],
83
+ // 다섯 번째 줄: 어두운 톤 (30% 밝기)
84
+ ['#cc0000', '#cc6600', '#cccc00', '#66cc00', '#00cccc', '#0066cc', '#0000cc', '#6600cc', '#cc00cc', '#333333'],
85
+ // 여섯 번째 줄: 매우 어두운 톤 (15% 밝기)
86
+ ['#800000', '#804000', '#808000', '#408000', '#008080', '#004080', '#000080', '#400080', '#800080', '#1a1a1a'],
87
+ ];
88
+ // 정렬 옵션 정의
89
+ const alignOptions = [
90
+ { value: 'left', label: '왼쪽 정렬', icon: 'alignLeft' },
91
+ { value: 'center', label: '가운데 정렬', icon: 'alignCenter' },
92
+ { value: 'right', label: '오른쪽 정렬', icon: 'alignRight' },
93
+ ];
94
+ // 문단 형식 옵션 정의
95
+ const paragraphOptions = [
96
+ { value: 'h1', label: '제목 1' },
97
+ { value: 'h2', label: '제목 2' },
98
+ { value: 'h3', label: '제목 3' },
99
+ { value: 'p', label: '본문', className: styles.pDefault },
100
+ { value: 'p1', label: 'P1', className: styles.p1Preview },
101
+ { value: 'p2', label: 'P2', className: styles.p2Preview },
102
+ { value: 'p3', label: 'P3', className: styles.p3Preview },
103
+ { value: 'p3_semibold', label: 'P3 Semibold', className: styles.p3_semiboldPreview },
104
+ { value: 'p4', label: 'P4', className: styles.p4Preview },
105
+ { value: 'p4_semibold', label: 'P4 Semibold', className: styles.p4_semiboldPreview },
106
+ { value: 'p5', label: 'P5', className: styles.p5Preview },
107
+ { value: 'p5_semibold', label: 'P5 Semibold', className: styles.p5_semiboldPreview },
108
+ ];
109
+ // 현재 선택된 스타일의 라벨 가져오기
110
+ const getCurrentStyleLabel = () => {
111
+ const option = paragraphOptions.find(opt => opt.value === currentParagraphStyle);
112
+ return option ? option.label : '문단 형식';
113
+ };
114
+ // 현재 정렬 상태의 라벨 가져오기
115
+ const getCurrentAlignLabel = () => {
116
+ const option = alignOptions.find(opt => opt.value === currentAlign);
117
+ return option ? option.label : '왼쪽 정렬';
118
+ };
119
+ // 현재 정렬 상태의 아이콘 가져오기
120
+ const getCurrentAlignIcon = () => {
121
+ const option = alignOptions.find(opt => opt.value === currentAlign);
122
+ return option ? styles[option.icon] : styles.alignLeft;
123
+ };
124
+ const validateHandler = (content) => {
125
+ setMessage('');
126
+ setStatusClass('');
127
+ if (validator && content.length > 0) {
128
+ try {
129
+ validator.parse(content);
130
+ setStatusClass('success');
131
+ }
132
+ catch (e) {
133
+ if (e instanceof z.ZodError) {
134
+ setMessage(e.errors[0].message);
135
+ setStatusClass('danger');
136
+ }
137
+ }
138
+ }
139
+ };
140
+ const detectCurrentAlign = () => {
141
+ // 정렬 상태 감지
142
+ if (document.queryCommandState('justifyLeft')) {
143
+ setCurrentAlign('left');
144
+ }
145
+ else if (document.queryCommandState('justifyCenter')) {
146
+ setCurrentAlign('center');
147
+ }
148
+ else if (document.queryCommandState('justifyRight')) {
149
+ setCurrentAlign('right');
150
+ }
151
+ else {
152
+ // 기본값은 왼쪽 정렬
153
+ setCurrentAlign('left');
154
+ }
155
+ };
156
+ const detectCurrentParagraphStyle = () => {
157
+ const selection = window.getSelection();
158
+ if (!selection || selection.rangeCount === 0) {
159
+ setCurrentParagraphStyle('p');
160
+ return;
161
+ }
162
+ let container = selection.getRangeAt(0).commonAncestorContainer;
163
+ if (container.nodeType === Node.TEXT_NODE) {
164
+ container = container.parentNode;
165
+ }
166
+ // 상위 블록 요소 찾기
167
+ while (container && container !== editorRef.current) {
168
+ const element = container;
169
+ if (element.tagName) {
170
+ const tagName = element.tagName.toLowerCase();
171
+ // H1, H2, H3 체크
172
+ if (tagName === 'h1' || tagName === 'h2' || tagName === 'h3') {
173
+ setCurrentParagraphStyle(tagName);
174
+ return;
175
+ }
176
+ // P 태그 체크
177
+ if (tagName === 'p') {
178
+ // 클래스 확인
179
+ if (element.className) {
180
+ // p1, p2, p3, p4, p5, p1_semibold, p2_semibold 등의 클래스 찾기
181
+ const classNames = Object.keys(styles);
182
+ for (const className of classNames) {
183
+ if (className.match(/^p[1-5](_semibold)?$/) &&
184
+ element.classList.contains(styles[className])) {
185
+ setCurrentParagraphStyle(className);
186
+ return;
187
+ }
188
+ }
189
+ }
190
+ // 클래스가 없으면 일반 p
191
+ setCurrentParagraphStyle('p');
192
+ return;
193
+ }
194
+ // DIV나 기타 블록 요소는 본문으로 처리
195
+ if (tagName === 'div' || tagName === 'blockquote' || tagName === 'pre') {
196
+ setCurrentParagraphStyle('p');
197
+ return;
198
+ }
199
+ }
200
+ container = container.parentNode;
201
+ }
202
+ // 기본값은 본문
203
+ setCurrentParagraphStyle('p');
204
+ };
205
+ // ref와 state 동기화
206
+ useEffect(() => {
207
+ historyRef.current = history;
208
+ historyIndexRef.current = historyIndex;
209
+ }, [history, historyIndex]);
210
+ // 히스토리에 추가 (디바운스 적용)
211
+ const addToHistory = useCallback((content) => {
212
+ // 기존 타이머 취소
213
+ if (historyTimerRef.current) {
214
+ clearTimeout(historyTimerRef.current);
215
+ }
216
+ // 500ms 후에 히스토리 추가 (연속 입력 시 하나로 묶음)
217
+ historyTimerRef.current = setTimeout(() => {
218
+ // ref에서 최신 값 가져오기
219
+ const currentHistory = historyRef.current;
220
+ const currentIndex = historyIndexRef.current;
221
+ // 현재 인덱스 이후의 히스토리 제거
222
+ const newHistory = currentHistory.slice(0, currentIndex + 1);
223
+ // 마지막 항목과 동일하면 추가하지 않음
224
+ if (newHistory[newHistory.length - 1] === content) {
225
+ return;
226
+ }
227
+ // 새 항목 추가 (최대 200개)
228
+ const updated = [...newHistory, content];
229
+ if (updated.length > 200) {
230
+ updated.shift(); // 가장 오래된 항목 제거
231
+ setHistory(updated);
232
+ setHistoryIndex(currentIndex); // 인덱스는 그대로 유지
233
+ }
234
+ else {
235
+ setHistory(updated);
236
+ setHistoryIndex(updated.length - 1);
237
+ }
238
+ }, 500);
239
+ }, []);
240
+ // Undo 실행
241
+ const performUndo = useCallback(() => {
242
+ // debounce 타이머 취소 (undo 중에는 히스토리 추가 안 함)
243
+ if (historyTimerRef.current) {
244
+ clearTimeout(historyTimerRef.current);
245
+ historyTimerRef.current = null;
246
+ }
247
+ const currentIndex = historyIndexRef.current;
248
+ const currentHistory = historyRef.current;
249
+ if (currentIndex > 0) {
250
+ const newIndex = currentIndex - 1;
251
+ const content = currentHistory[newIndex];
252
+ setHistoryIndex(newIndex);
253
+ if (editorRef.current) {
254
+ isUndoRedoRef.current = true; // undo 실행 중 플래그 설정
255
+ editorRef.current.innerHTML = content;
256
+ onChange(content);
257
+ detectCurrentParagraphStyle();
258
+ detectCurrentAlign();
259
+ // 다음 틱에서 플래그 해제
260
+ setTimeout(() => {
261
+ isUndoRedoRef.current = false;
262
+ }, 0);
263
+ }
264
+ }
265
+ }, [onChange]);
266
+ // Redo 실행
267
+ const performRedo = useCallback(() => {
268
+ // debounce 타이머 취소 (redo 중에는 히스토리 추가 안 함)
269
+ if (historyTimerRef.current) {
270
+ clearTimeout(historyTimerRef.current);
271
+ historyTimerRef.current = null;
272
+ }
273
+ const currentIndex = historyIndexRef.current;
274
+ const currentHistory = historyRef.current;
275
+ if (currentIndex < currentHistory.length - 1) {
276
+ const newIndex = currentIndex + 1;
277
+ const content = currentHistory[newIndex];
278
+ setHistoryIndex(newIndex);
279
+ if (editorRef.current) {
280
+ isUndoRedoRef.current = true; // redo 실행 중 플래그 설정
281
+ editorRef.current.innerHTML = content;
282
+ onChange(content);
283
+ detectCurrentParagraphStyle();
284
+ detectCurrentAlign();
285
+ // 다음 틱에서 플래그 해제
286
+ setTimeout(() => {
287
+ isUndoRedoRef.current = false;
288
+ }, 0);
289
+ }
290
+ }
291
+ }, [onChange]);
292
+ const handleInput = useCallback(() => {
293
+ if (editorRef.current) {
294
+ const content = editorRef.current.innerHTML;
295
+ onChange(content);
296
+ validateHandler(content);
297
+ detectCurrentParagraphStyle();
298
+ detectCurrentAlign();
299
+ // 히스토리에 추가
300
+ addToHistory(content);
301
+ }
302
+ // eslint-disable-next-line react-hooks/exhaustive-deps
303
+ }, [onChange, addToHistory]);
304
+ // 붙여넣기 이벤트 핸들러 - 지원하지 않는 스타일 제거
305
+ const handlePaste = useCallback((e) => {
306
+ e.preventDefault();
307
+ const clipboardData = e.clipboardData;
308
+ if (!clipboardData)
309
+ return;
310
+ // HTML 데이터 가져오기
311
+ const html = clipboardData.getData('text/html');
312
+ const text = clipboardData.getData('text/plain');
313
+ if (html) {
314
+ // 임시 div를 만들어서 HTML 파싱
315
+ const tempDiv = document.createElement('div');
316
+ tempDiv.innerHTML = html;
317
+ // 지원하는 태그와 스타일 정의
318
+ const allowedTags = ['P', 'BR', 'STRONG', 'B', 'EM', 'I', 'U', 'S', 'STRIKE', 'DEL',
319
+ 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'PRE',
320
+ 'UL', 'OL', 'LI', 'A', 'IMG', 'SPAN', 'DIV'];
321
+ const allowedStyles = ['color', 'background-color', 'text-align'];
322
+ // 모든 요소를 순회하면서 정리
323
+ const cleanElement = (element) => {
324
+ const tagName = element.tagName;
325
+ // 지원하지 않는 태그는 내용만 유지
326
+ if (!allowedTags.includes(tagName)) {
327
+ const fragment = document.createDocumentFragment();
328
+ Array.from(element.childNodes).forEach(child => {
329
+ if (child.nodeType === Node.ELEMENT_NODE) {
330
+ const cleaned = cleanElement(child);
331
+ if (cleaned)
332
+ fragment.appendChild(cleaned);
333
+ }
334
+ else if (child.nodeType === Node.TEXT_NODE) {
335
+ fragment.appendChild(child.cloneNode(true));
336
+ }
337
+ });
338
+ return fragment.childNodes.length > 0 ? fragment : null;
339
+ }
340
+ // 지원하는 태그는 복제하고 스타일 정리
341
+ const newElement = element.cloneNode(false);
342
+ // 모든 속성 제거 후 필요한 것만 복원
343
+ const attrs = Array.from(element.attributes);
344
+ attrs.forEach(attr => newElement.removeAttribute(attr.name));
345
+ // href, src, alt 등 필수 속성만 복원
346
+ if (tagName === 'A' && element.getAttribute('href')) {
347
+ newElement.setAttribute('href', element.getAttribute('href'));
348
+ }
349
+ if (tagName === 'IMG') {
350
+ if (element.getAttribute('src')) {
351
+ newElement.setAttribute('src', element.getAttribute('src'));
352
+ }
353
+ if (element.getAttribute('alt')) {
354
+ newElement.setAttribute('alt', element.getAttribute('alt'));
355
+ }
356
+ }
357
+ // 스타일 복원 (허용된 것만)
358
+ if (element instanceof HTMLElement && element.style) {
359
+ allowedStyles.forEach(styleName => {
360
+ const value = element.style.getPropertyValue(styleName);
361
+ if (value) {
362
+ newElement.style.setProperty(styleName, value);
363
+ }
364
+ });
365
+ }
366
+ // 자식 요소 처리
367
+ Array.from(element.childNodes).forEach(child => {
368
+ if (child.nodeType === Node.ELEMENT_NODE) {
369
+ const cleaned = cleanElement(child);
370
+ if (cleaned)
371
+ newElement.appendChild(cleaned);
372
+ }
373
+ else if (child.nodeType === Node.TEXT_NODE) {
374
+ newElement.appendChild(child.cloneNode(true));
375
+ }
376
+ });
377
+ return newElement;
378
+ };
379
+ // 정리된 HTML 생성
380
+ const cleanedDiv = document.createElement('div');
381
+ Array.from(tempDiv.childNodes).forEach(child => {
382
+ if (child.nodeType === Node.ELEMENT_NODE) {
383
+ const cleaned = cleanElement(child);
384
+ if (cleaned)
385
+ cleanedDiv.appendChild(cleaned);
386
+ }
387
+ else if (child.nodeType === Node.TEXT_NODE) {
388
+ cleanedDiv.appendChild(child.cloneNode(true));
389
+ }
390
+ });
391
+ // 현재 커서 위치에 삽입
392
+ const selection = window.getSelection();
393
+ if (selection && selection.rangeCount > 0) {
394
+ const range = selection.getRangeAt(0);
395
+ range.deleteContents();
396
+ const fragment = document.createDocumentFragment();
397
+ while (cleanedDiv.firstChild) {
398
+ fragment.appendChild(cleanedDiv.firstChild);
399
+ }
400
+ range.insertNode(fragment);
401
+ // 커서를 삽입된 내용의 끝으로 이동
402
+ range.collapse(false);
403
+ selection.removeAllRanges();
404
+ selection.addRange(range);
405
+ }
406
+ }
407
+ else if (text) {
408
+ // HTML이 없으면 일반 텍스트 삽입
409
+ const selection = window.getSelection();
410
+ if (selection && selection.rangeCount > 0) {
411
+ const range = selection.getRangeAt(0);
412
+ range.deleteContents();
413
+ range.insertNode(document.createTextNode(text));
414
+ range.collapse(false);
415
+ }
416
+ }
417
+ // 변경사항 반영
418
+ handleInput();
419
+ }, [handleInput]);
420
+ const execCommand = (command, value = undefined) => {
421
+ // undo/redo는 커스텀 함수 사용
422
+ if (command === 'undo') {
423
+ performUndo();
424
+ return;
425
+ }
426
+ if (command === 'redo') {
427
+ performRedo();
428
+ return;
429
+ }
430
+ // bold, italic, underline, strikeThrough일 때 선택 영역이 없으면 아무것도 하지 않음
431
+ if (['bold', 'italic', 'underline', 'strikeThrough'].includes(command)) {
432
+ const selection = window.getSelection();
433
+ if (selection && selection.isCollapsed) {
434
+ // 선택 영역이 없으면 실행하지 않음
435
+ return;
436
+ }
437
+ }
438
+ document.execCommand(command, false, value);
439
+ editorRef.current?.focus();
440
+ handleInput();
441
+ };
442
+ // 코드보기 토글
443
+ const toggleCodeView = () => {
444
+ if (isCodeView) {
445
+ // 코드보기에서 일반 모드로 전환
446
+ setIsCodeView(false);
447
+ setSavedEditorHeight(null); // 저장된 높이 초기화
448
+ // 다음 렌더링 사이클에서 에디터 내용 업데이트
449
+ setTimeout(() => {
450
+ if (editorRef.current && codeContent !== undefined) {
451
+ editorRef.current.innerHTML = codeContent;
452
+ handleInput();
453
+ }
454
+ }, 0);
455
+ }
456
+ else {
457
+ // 일반 모드에서 코드보기로 전환
458
+ if (editorRef.current) {
459
+ // height가 contents일 때 현재 에디터 높이 저장
460
+ if (height === 'contents') {
461
+ const currentHeight = editorRef.current.scrollHeight;
462
+ setSavedEditorHeight(currentHeight);
463
+ }
464
+ // 현재 HTML을 포맷팅
465
+ const html = editorRef.current.innerHTML;
466
+ const formattedHtml = formatHtml(html);
467
+ setCodeContent(formattedHtml);
468
+ setIsCodeView(true);
469
+ }
470
+ }
471
+ };
472
+ // HTML 포맷팅 함수
473
+ const formatHtml = (html) => {
474
+ // 기본적인 HTML 포맷팅
475
+ const formatted = html
476
+ .replace(/></g, '>\n<') // 태그 사이에 줄바꿈 추가
477
+ .replace(/(<div|<p|<h[1-6]|<ul|<ol|<li|<blockquote)/gi, '\n$1') // 블록 요소 앞에 줄바꿈
478
+ .replace(/(<\/div>|<\/p>|<\/h[1-6]>|<\/ul>|<\/ol>|<\/li>|<\/blockquote>)/gi, '$1\n'); // 블록 요소 뒤에 줄바꿈
479
+ // 들여쓰기 추가
480
+ const lines = formatted.split('\n');
481
+ let indentLevel = 0;
482
+ const indentedLines = lines.map(line => {
483
+ const trimmed = line.trim();
484
+ if (!trimmed)
485
+ return '';
486
+ // 닫는 태그인 경우 들여쓰기 레벨 감소
487
+ if (trimmed.startsWith('</')) {
488
+ indentLevel = Math.max(0, indentLevel - 1);
489
+ const indented = ' '.repeat(indentLevel) + trimmed;
490
+ return indented;
491
+ }
492
+ // 자체 닫는 태그가 아닌 여는 태그인 경우
493
+ const indented = ' '.repeat(indentLevel) + trimmed;
494
+ if (trimmed.startsWith('<') && !trimmed.startsWith('<!') &&
495
+ !trimmed.endsWith('/>') && trimmed.includes('>') &&
496
+ !trimmed.includes('</')) {
497
+ indentLevel++;
498
+ }
499
+ return indented;
500
+ });
501
+ return indentedLines.filter(line => line !== '').join('\n');
502
+ };
503
+ // 코드 에디터 내용 변경 처리
504
+ const handleCodeChange = (e) => {
505
+ setCodeContent(e.target.value);
506
+ };
507
+ const applyParagraphStyle = (value) => {
508
+ // 빈 값이면 본문으로 설정
509
+ if (!value) {
510
+ value = 'p';
511
+ }
512
+ // h1, h2, h3는 formatBlock 사용
513
+ if (value === 'h1' || value === 'h2' || value === 'h3') {
514
+ execCommand('formatBlock', value);
515
+ setCurrentParagraphStyle(value);
516
+ }
517
+ // 본문은 p 태그로
518
+ else if (value === 'p') {
519
+ execCommand('formatBlock', 'p');
520
+ setCurrentParagraphStyle('p');
521
+ }
522
+ // p1~p5 및 p1_semibold~p5_semibold 스타일은 클래스 적용
523
+ else if (value.match(/^p[1-5](_semibold)?$/)) {
524
+ // 먼저 p 태그로 만들고
525
+ execCommand('formatBlock', 'p');
526
+ // 잠시 후 클래스 적용
527
+ setTimeout(() => {
528
+ const selection = window.getSelection();
529
+ if (!selection || selection.rangeCount === 0)
530
+ return;
531
+ let container = selection.getRangeAt(0).commonAncestorContainer;
532
+ if (container.nodeType === Node.TEXT_NODE) {
533
+ container = container.parentNode;
534
+ }
535
+ // 상위 블록 요소 찾기
536
+ while (container && container !== editorRef.current) {
537
+ const element = container;
538
+ if (element.tagName && ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'DIV'].includes(element.tagName)) {
539
+ // 클래스 적용
540
+ element.className = styles[value];
541
+ setCurrentParagraphStyle(value);
542
+ break;
543
+ }
544
+ container = element.parentNode;
545
+ }
546
+ handleInput();
547
+ }, 10);
548
+ }
549
+ // 드롭다운 닫기
550
+ setIsParagraphDropdownOpen(false);
551
+ };
552
+ const applyLink = () => {
553
+ if (linkUrl && savedSelection) {
554
+ restoreSelection(savedSelection);
555
+ const selection = window.getSelection();
556
+ if (selection && !selection.isCollapsed) {
557
+ const range = selection.getRangeAt(0);
558
+ const selectedText = range.toString();
559
+ // Create link element
560
+ const link = document.createElement('a');
561
+ link.href = linkUrl;
562
+ link.textContent = selectedText;
563
+ // Set target attribute
564
+ if (linkTarget === '_blank') {
565
+ link.target = '_blank';
566
+ link.rel = 'noopener noreferrer';
567
+ }
568
+ // Replace selection with link
569
+ range.deleteContents();
570
+ range.insertNode(link);
571
+ // Clear and close dropdown
572
+ setLinkUrl('');
573
+ setLinkTarget('_blank');
574
+ setIsLinkDropdownOpen(false);
575
+ setSavedSelection(null);
576
+ editorRef.current?.focus();
577
+ handleInput();
578
+ }
579
+ }
580
+ };
581
+ const openLinkDropdown = () => {
582
+ const selection = window.getSelection();
583
+ if (selection && !selection.isCollapsed) {
584
+ // Save selection
585
+ const range = saveSelection();
586
+ setSavedSelection(range);
587
+ setIsLinkDropdownOpen(true);
588
+ setIsParagraphDropdownOpen(false);
589
+ setIsTextColorOpen(false);
590
+ setIsBgColorOpen(false);
591
+ setIsAlignDropdownOpen(false);
592
+ }
593
+ };
594
+ // 이미지 선택
595
+ const selectImage = (img) => {
596
+ // 기존 선택 해제
597
+ if (selectedImage) {
598
+ deselectImage();
599
+ }
600
+ setSelectedImage(img);
601
+ // 이미지 주위에 wrapper 추가
602
+ const wrapper = document.createElement('div');
603
+ wrapper.className = 'image-wrapper';
604
+ wrapper.style.position = 'relative';
605
+ wrapper.style.display = 'inline-block';
606
+ wrapper.style.border = '2px solid #0084ff';
607
+ wrapper.style.padding = '0';
608
+ // 리사이즈 핸들 추가
609
+ const handles = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
610
+ handles.forEach(handle => {
611
+ const handleDiv = document.createElement('div');
612
+ handleDiv.className = `resize-handle resize-handle-${handle}`;
613
+ handleDiv.dataset.handle = handle;
614
+ handleDiv.style.position = 'absolute';
615
+ handleDiv.style.width = '8px';
616
+ handleDiv.style.height = '8px';
617
+ handleDiv.style.backgroundColor = '#0084ff';
618
+ handleDiv.style.border = '1px solid white';
619
+ handleDiv.style.borderRadius = '2px';
620
+ handleDiv.style.cursor = `${handle}-resize`;
621
+ // 핸들 위치 설정
622
+ switch (handle) {
623
+ case 'nw':
624
+ handleDiv.style.top = '-5px';
625
+ handleDiv.style.left = '-5px';
626
+ break;
627
+ case 'n':
628
+ handleDiv.style.top = '-5px';
629
+ handleDiv.style.left = '50%';
630
+ handleDiv.style.transform = 'translateX(-50%)';
631
+ break;
632
+ case 'ne':
633
+ handleDiv.style.top = '-5px';
634
+ handleDiv.style.right = '-5px';
635
+ break;
636
+ case 'e':
637
+ handleDiv.style.top = '50%';
638
+ handleDiv.style.right = '-5px';
639
+ handleDiv.style.transform = 'translateY(-50%)';
640
+ break;
641
+ case 'se':
642
+ handleDiv.style.bottom = '-5px';
643
+ handleDiv.style.right = '-5px';
644
+ break;
645
+ case 's':
646
+ handleDiv.style.bottom = '-5px';
647
+ handleDiv.style.left = '50%';
648
+ handleDiv.style.transform = 'translateX(-50%)';
649
+ break;
650
+ case 'sw':
651
+ handleDiv.style.bottom = '-5px';
652
+ handleDiv.style.left = '-5px';
653
+ break;
654
+ case 'w':
655
+ handleDiv.style.top = '50%';
656
+ handleDiv.style.left = '-5px';
657
+ handleDiv.style.transform = 'translateY(-50%)';
658
+ break;
659
+ }
660
+ // 리사이즈 이벤트 핸들러
661
+ handleDiv.onmousedown = (e) => {
662
+ e.preventDefault();
663
+ e.stopPropagation();
664
+ startResize(e, img, handle);
665
+ };
666
+ wrapper.appendChild(handleDiv);
667
+ });
668
+ // 이미지를 wrapper로 감싸기
669
+ const parent = img.parentNode;
670
+ parent?.insertBefore(wrapper, img);
671
+ wrapper.appendChild(img);
672
+ // 편집 팝업 데이터 설정
673
+ // 이미지 크기 확인
674
+ if (img.style.width) {
675
+ setEditImageWidth(img.style.width);
676
+ }
677
+ else {
678
+ setEditImageWidth('original');
679
+ }
680
+ // 이미지의 정렬 상태 확인 - 부모 div의 textAlign 체크
681
+ let container = img.parentElement;
682
+ let currentAlign = 'left'; // 기본값
683
+ // 부모 요소를 올라가며 textAlign이 설정된 div 찾기
684
+ while (container && container !== editorRef.current) {
685
+ if (container.tagName === 'DIV' && container.style.textAlign) {
686
+ currentAlign = container.style.textAlign;
687
+ break;
688
+ }
689
+ container = container.parentElement;
690
+ }
691
+ setEditImageAlign(currentAlign);
692
+ setEditImageAlt(img.alt || '');
693
+ // 약간의 지연 후 편집창 열기 (클릭 이벤트 완전 처리 후)
694
+ setTimeout(() => {
695
+ setIsImageEditPopupOpen(true);
696
+ }, 50);
697
+ };
698
+ // 이미지 선택 해제
699
+ const deselectImage = () => {
700
+ if (!selectedImage)
701
+ return;
702
+ // wrapper 제거
703
+ const wrapper = selectedImage.parentElement;
704
+ if (wrapper && wrapper.classList.contains('image-wrapper')) {
705
+ const parent = wrapper.parentNode;
706
+ if (parent) {
707
+ try {
708
+ // 이미지를 wrapper 밖으로 이동
709
+ parent.insertBefore(selectedImage, wrapper);
710
+ // wrapper 제거
711
+ wrapper.remove();
712
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
713
+ }
714
+ catch (e) {
715
+ // 이미 제거된 경우 무시
716
+ }
717
+ }
718
+ }
719
+ // 이미지 draggable 속성 제거
720
+ if (selectedImage) {
721
+ selectedImage.draggable = false;
722
+ }
723
+ // 상태 초기화
724
+ setSelectedImage(null);
725
+ setIsImageEditPopupOpen(false);
726
+ setIsResizing(false);
727
+ setResizeStartData(null);
728
+ };
729
+ // 리사이즈 시작
730
+ const startResize = (e, img, handle) => {
731
+ setIsResizing(true);
732
+ setResizeStartData({
733
+ startX: e.clientX,
734
+ startY: e.clientY,
735
+ startWidth: img.offsetWidth,
736
+ startHeight: img.offsetHeight,
737
+ handle
738
+ });
739
+ };
740
+ // 이미지 편집 적용
741
+ const applyImageEdit = () => {
742
+ if (!selectedImage)
743
+ return;
744
+ // 크기 적용
745
+ if (editImageWidth) {
746
+ if (editImageWidth.includes('%')) {
747
+ selectedImage.style.width = editImageWidth;
748
+ selectedImage.style.height = 'auto';
749
+ }
750
+ else if (editImageWidth === 'original') {
751
+ selectedImage.style.width = '';
752
+ selectedImage.style.height = '';
753
+ }
754
+ else {
755
+ selectedImage.style.width = editImageWidth;
756
+ selectedImage.style.height = 'auto';
757
+ }
758
+ }
759
+ // 정렬 적용 - 이미지를 감싸는 정렬 컨테이너 찾기 또는 생성
760
+ let alignContainer = selectedImage.parentElement;
761
+ // wrapper가 있으면 그 부모를 확인
762
+ if (alignContainer?.classList.contains('image-wrapper')) {
763
+ alignContainer = alignContainer.parentElement;
764
+ }
765
+ // 정렬 컨테이너가 이미 있는지 확인 (div이고 textAlign이 설정된 경우)
766
+ if (alignContainer && alignContainer.tagName === 'DIV' && alignContainer !== editorRef.current) {
767
+ // 기존 컨테이너의 정렬 변경
768
+ alignContainer.style.textAlign = editImageAlign;
769
+ }
770
+ else {
771
+ // 정렬 컨테이너가 없으면 새로 생성
772
+ const newContainer = document.createElement('div');
773
+ newContainer.style.textAlign = editImageAlign;
774
+ // wrapper나 이미지를 새 컨테이너로 감싸기
775
+ const elementToWrap = selectedImage.parentElement?.classList.contains('image-wrapper')
776
+ ? selectedImage.parentElement
777
+ : selectedImage;
778
+ if (elementToWrap.parentNode) {
779
+ elementToWrap.parentNode.insertBefore(newContainer, elementToWrap);
780
+ newContainer.appendChild(elementToWrap);
781
+ }
782
+ }
783
+ // 대체 텍스트 적용
784
+ selectedImage.alt = editImageAlt;
785
+ // 선택 해제
786
+ deselectImage();
787
+ handleInput();
788
+ };
789
+ // 이미지 삭제
790
+ const deleteImage = () => {
791
+ if (!selectedImage)
792
+ return;
793
+ // 먼저 선택 해제 (상태 초기화)
794
+ const imageToDelete = selectedImage;
795
+ deselectImage();
796
+ // wrapper가 있는 경우 wrapper를 찾아서 제거
797
+ let elementToRemove = imageToDelete;
798
+ let parent = imageToDelete.parentElement;
799
+ // wrapper를 거슬러 올라가며 정렬 컨테이너까지 찾기
800
+ while (parent && parent !== editorRef.current) {
801
+ if (parent.classList.contains('image-wrapper') ||
802
+ (parent.tagName === 'DIV' && parent.style.textAlign)) {
803
+ elementToRemove = parent;
804
+ parent = parent.parentElement;
805
+ }
806
+ else {
807
+ break;
808
+ }
809
+ }
810
+ // DOM에서 제거
811
+ if (elementToRemove.parentNode) {
812
+ elementToRemove.parentNode.removeChild(elementToRemove);
813
+ }
814
+ handleInput();
815
+ };
816
+ // 링크 요소 클릭 감지
817
+ const handleEditorClick = (e) => {
818
+ const target = e.target;
819
+ // 리사이즈 핸들 클릭은 무시
820
+ if (target.classList.contains('resize-handle')) {
821
+ return;
822
+ }
823
+ // 이미지 편집 팝업 클릭은 무시
824
+ if (target.closest(`.${styles.imageDropdown}`)) {
825
+ return;
826
+ }
827
+ // 유튜브 편집 팝업 클릭은 무시
828
+ if (target.closest(`.${styles.youtubeEditPopup}`)) {
829
+ return;
830
+ }
831
+ // 유튜브 오버레이 클릭 감지
832
+ if ((target.classList.contains('youtube-overlay') || target.closest('.youtube-container')) && editorRef.current?.contains(target)) {
833
+ e.preventDefault();
834
+ e.stopPropagation();
835
+ const youtubeContainer = target.closest('.youtube-container');
836
+ if (youtubeContainer) {
837
+ // 이미 선택된 유튜브가 아닌 경우에만 선택
838
+ if (selectedYoutube !== youtubeContainer) {
839
+ // 기존 선택 해제
840
+ if (selectedYoutube) {
841
+ deselectYoutube();
842
+ }
843
+ if (selectedImage) {
844
+ deselectImage();
845
+ }
846
+ selectYoutube(youtubeContainer);
847
+ }
848
+ else {
849
+ // 같은 유튜브를 다시 클릭하면 편집창 토글
850
+ setIsYoutubeEditPopupOpen(!isYoutubeEditPopupOpen);
851
+ }
852
+ }
853
+ return;
854
+ }
855
+ // 이미지 요소인지 확인
856
+ if (target.tagName === 'IMG' && editorRef.current?.contains(target)) {
857
+ e.preventDefault();
858
+ e.stopPropagation();
859
+ const img = target;
860
+ // 이미 선택된 이미지가 아닌 경우에만 선택
861
+ if (selectedImage !== img) {
862
+ // 기존 선택 해제
863
+ if (selectedImage) {
864
+ deselectImage();
865
+ }
866
+ if (selectedYoutube) {
867
+ deselectYoutube();
868
+ }
869
+ selectImage(img);
870
+ }
871
+ else {
872
+ // 같은 이미지를 다시 클릭하면 편집창 토글
873
+ setIsImageEditPopupOpen(!isImageEditPopupOpen);
874
+ }
875
+ return;
876
+ }
877
+ // 기존 선택된 이미지가 있으면 선택 해제
878
+ // image-wrapper 또는 리사이즈 핸들이 아닌 경우
879
+ // 단, 리사이즈 중일 때는 선택 해제하지 않음
880
+ if (selectedImage && !target.closest('.image-wrapper') && !isResizing) {
881
+ deselectImage();
882
+ }
883
+ // 기존 선택된 유튜브가 있으면 선택 해제
884
+ // 단, 리사이즈 중일 때는 선택 해제하지 않음
885
+ if (selectedYoutube && !target.closest('.youtube-wrapper') && !isResizing) {
886
+ deselectYoutube();
887
+ }
888
+ // 링크 요소인지 확인
889
+ const linkElement = target.closest('a');
890
+ if (linkElement && editorRef.current?.contains(linkElement)) {
891
+ e.preventDefault();
892
+ setSelectedLinkElement(linkElement);
893
+ setEditLinkUrl(linkElement.href);
894
+ setEditLinkTarget(linkElement.target || '_self');
895
+ setIsEditLinkPopupOpen(true);
896
+ }
897
+ else {
898
+ // 일반 클릭 처리
899
+ detectCurrentParagraphStyle();
900
+ detectCurrentAlign();
901
+ }
902
+ };
903
+ // 링크 수정
904
+ const updateLink = () => {
905
+ if (selectedLinkElement && editLinkUrl) {
906
+ selectedLinkElement.href = editLinkUrl;
907
+ if (editLinkTarget === '_blank') {
908
+ selectedLinkElement.target = '_blank';
909
+ selectedLinkElement.rel = 'noopener noreferrer';
910
+ }
911
+ else {
912
+ selectedLinkElement.removeAttribute('target');
913
+ selectedLinkElement.removeAttribute('rel');
914
+ }
915
+ setIsEditLinkPopupOpen(false);
916
+ setSelectedLinkElement(null);
917
+ editorRef.current?.focus();
918
+ handleInput();
919
+ }
920
+ };
921
+ // 링크 삭제
922
+ const removeLink = () => {
923
+ if (selectedLinkElement) {
924
+ const parent = selectedLinkElement.parentNode;
925
+ const textContent = selectedLinkElement.textContent || '';
926
+ const textNode = document.createTextNode(textContent);
927
+ parent?.replaceChild(textNode, selectedLinkElement);
928
+ setIsEditLinkPopupOpen(false);
929
+ setSelectedLinkElement(null);
930
+ editorRef.current?.focus();
931
+ handleInput();
932
+ }
933
+ };
934
+ const openImageDropdown = (e) => {
935
+ e?.stopPropagation();
936
+ // 현재 선택 영역 저장
937
+ const selection = window.getSelection();
938
+ if (selection && selection.rangeCount > 0) {
939
+ const range = selection.getRangeAt(0).cloneRange();
940
+ setSavedImageSelection(range);
941
+ }
942
+ else {
943
+ setSavedImageSelection(null);
944
+ }
945
+ setIsImageDropdownOpen(true);
946
+ setImageTabMode('file'); // 기본값으로 파일 업로드 탭 선택
947
+ setIsParagraphDropdownOpen(false);
948
+ setIsTextColorOpen(false);
949
+ setIsBgColorOpen(false);
950
+ setIsAlignDropdownOpen(false);
951
+ setIsLinkDropdownOpen(false);
952
+ };
953
+ const insertImage = async () => {
954
+ let imageSrc = '';
955
+ // 파일이 업로드된 경우
956
+ if (imageFile && imagePreview) {
957
+ imageSrc = imagePreview;
958
+ }
959
+ // URL이 입력된 경우
960
+ else if (imageUrl) {
961
+ // URL 유효성 검사
962
+ try {
963
+ const testImg = new Image();
964
+ await new Promise((resolve, reject) => {
965
+ const timeout = setTimeout(() => {
966
+ reject(new Error('Timeout'));
967
+ }, 5000); // 5초 타임아웃
968
+ testImg.onload = () => {
969
+ clearTimeout(timeout);
970
+ resolve(true);
971
+ };
972
+ testImg.onerror = () => {
973
+ clearTimeout(timeout);
974
+ reject(new Error('Load failed'));
975
+ };
976
+ // CORS를 우회하기 위해 crossOrigin 설정하지 않음
977
+ testImg.src = imageUrl;
978
+ });
979
+ imageSrc = imageUrl;
980
+ }
981
+ catch (error) {
982
+ console.error('Image validation failed:', error);
983
+ alert(`이미지를 불러올 수 없습니다.\n\n가능한 원인:\n1. 잘못된 이미지 URL\n2. CORS 정책으로 인한 차단 (외부 도메인)\n3. 네트워크 연결 문제\n4. 이미지가 존재하지 않음\n\nURL: ${imageUrl}\n\n💡 팁: CORS 정책으로 차단된 경우, 이미지를 직접 다운로드 후 파일 업로드를 사용해주세요.`);
984
+ return;
985
+ }
986
+ }
987
+ if (!imageSrc)
988
+ return;
989
+ // 이미지 엘리먼트 생성
990
+ const img = document.createElement('img');
991
+ img.src = imageSrc;
992
+ img.alt = imageAlt || '';
993
+ // display를 inline-block으로 설정하여 정렬이 작동하도록 함
994
+ img.style.display = 'inline-block';
995
+ img.style.verticalAlign = 'middle'; // 수직 정렬 개선
996
+ // 이미지 로드 에러 처리
997
+ img.onerror = () => {
998
+ console.error('Image load failed:', imageSrc);
999
+ alert(`이미지를 불러올 수 없습니다.\n\n가능한 원인:\n1. 잘못된 이미지 URL\n2. CORS 정책으로 인한 차단\n3. 네트워크 연결 문제\n\nURL: ${imageSrc}`);
1000
+ // 에러 발생 시 삽입된 이미지 제거
1001
+ if (img.parentNode) {
1002
+ img.parentNode.removeChild(img);
1003
+ }
1004
+ };
1005
+ // 이미지 로드 성공 처리
1006
+ img.onload = () => {
1007
+ // 이미지 로드 성공
1008
+ };
1009
+ // 크기 설정
1010
+ if (imageWidth === '100%') {
1011
+ img.style.width = '100%';
1012
+ img.style.height = 'auto';
1013
+ }
1014
+ else if (imageWidth === '75%') {
1015
+ img.style.width = '75%';
1016
+ img.style.height = 'auto';
1017
+ }
1018
+ else if (imageWidth === '50%') {
1019
+ img.style.width = '50%';
1020
+ img.style.height = 'auto';
1021
+ }
1022
+ // '원본'인 경우 스타일을 설정하지 않음
1023
+ // 컨테이너 div 생성 (정렬용)
1024
+ const container = document.createElement('div');
1025
+ container.style.textAlign = imageAlign;
1026
+ container.appendChild(img);
1027
+ // 에디터에 포커스 먼저 설정
1028
+ if (editorRef.current) {
1029
+ editorRef.current.focus();
1030
+ const selection = window.getSelection();
1031
+ // 저장된 선택 영역이 있으면 복원
1032
+ if (savedImageSelection && selection) {
1033
+ try {
1034
+ selection.removeAllRanges();
1035
+ selection.addRange(savedImageSelection);
1036
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1037
+ }
1038
+ catch (e) {
1039
+ // 복원 실패 시 무시
1040
+ }
1041
+ }
1042
+ // 선택 영역 재확인
1043
+ if (!selection || selection.rangeCount === 0 || !editorRef.current.contains(selection.anchorNode)) {
1044
+ // 에디터가 비어있으면 p 태그 추가
1045
+ if (!editorRef.current.innerHTML || editorRef.current.innerHTML === '<br>') {
1046
+ const p = document.createElement('p');
1047
+ p.innerHTML = '<br>';
1048
+ editorRef.current.appendChild(p);
1049
+ }
1050
+ // 커서를 에디터 끝으로 이동
1051
+ const range = document.createRange();
1052
+ range.selectNodeContents(editorRef.current);
1053
+ range.collapse(false);
1054
+ selection?.removeAllRanges();
1055
+ selection?.addRange(range);
1056
+ }
1057
+ // 이제 이미지 삽입
1058
+ if (selection && selection.rangeCount > 0) {
1059
+ const range = selection.getRangeAt(0);
1060
+ range.deleteContents();
1061
+ range.insertNode(container);
1062
+ // 이미지 다음에 새 문단 추가
1063
+ const newP = document.createElement('p');
1064
+ newP.innerHTML = '<br>';
1065
+ container.after(newP);
1066
+ // 커서를 새 문단으로 이동
1067
+ const newRange = document.createRange();
1068
+ newRange.selectNodeContents(newP);
1069
+ newRange.collapse(true);
1070
+ selection.removeAllRanges();
1071
+ selection.addRange(newRange);
1072
+ }
1073
+ else {
1074
+ // 폴백: 에디터 끝에 추가
1075
+ editorRef.current.appendChild(container);
1076
+ }
1077
+ }
1078
+ // 상태 초기화
1079
+ setIsImageDropdownOpen(false);
1080
+ setImageTabMode('file'); // 탭 모드도 초기화
1081
+ setImageUrl('');
1082
+ setImageFile(null);
1083
+ setImagePreview('');
1084
+ setImageWidth('original'); // 원본으로 초기화
1085
+ setImageAlign('left'); // 좌측으로 초기화
1086
+ setImageAlt('');
1087
+ setSavedImageSelection(null); // 저장된 선택 영역 초기화
1088
+ editorRef.current?.focus();
1089
+ handleInput();
1090
+ };
1091
+ // YouTube URL에서 Video ID 추출
1092
+ const extractYoutubeVideoId = (url) => {
1093
+ // 다양한 YouTube URL 형식 지원
1094
+ const patterns = [
1095
+ /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/,
1096
+ /youtube\.com\/watch\?.*v=([^&\n?#]+)/,
1097
+ ];
1098
+ for (const pattern of patterns) {
1099
+ const match = url.match(pattern);
1100
+ if (match && match[1]) {
1101
+ return match[1];
1102
+ }
1103
+ }
1104
+ return null;
1105
+ };
1106
+ // YouTube 선택
1107
+ const selectYoutube = (youtubeContainer) => {
1108
+ // 기존 선택 해제
1109
+ if (selectedYoutube) {
1110
+ deselectYoutube();
1111
+ }
1112
+ if (selectedImage) {
1113
+ deselectImage();
1114
+ }
1115
+ setSelectedYoutube(youtubeContainer);
1116
+ // 유튜브 주위에 wrapper 추가
1117
+ const wrapper = document.createElement('div');
1118
+ wrapper.className = 'youtube-wrapper';
1119
+ wrapper.style.position = 'relative';
1120
+ wrapper.style.border = '2px solid #0084ff';
1121
+ wrapper.style.padding = '0';
1122
+ // wrapper를 유튜브 컨테이너와 동일한 display 속성으로 설정
1123
+ const computedStyle = window.getComputedStyle(youtubeContainer);
1124
+ wrapper.style.display = computedStyle.display;
1125
+ // style.width가 있으면 그것을 사용, 없으면 computed width 사용
1126
+ wrapper.style.width = youtubeContainer.style.width || computedStyle.width;
1127
+ // 원본 스타일 저장 (나중에 복원용)
1128
+ youtubeContainer.dataset.originalWidth = youtubeContainer.style.width;
1129
+ youtubeContainer.dataset.originalDisplay = youtubeContainer.style.display;
1130
+ // 리사이즈 핸들 추가 (8개 포인트)
1131
+ const handles = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
1132
+ handles.forEach(handle => {
1133
+ const handleDiv = document.createElement('div');
1134
+ handleDiv.className = `resize-handle resize-handle-${handle}`;
1135
+ handleDiv.dataset.handle = handle;
1136
+ handleDiv.style.position = 'absolute';
1137
+ handleDiv.style.width = '8px';
1138
+ handleDiv.style.height = '8px';
1139
+ handleDiv.style.backgroundColor = '#0084ff';
1140
+ handleDiv.style.border = '1px solid white';
1141
+ handleDiv.style.borderRadius = '2px';
1142
+ handleDiv.style.cursor = `${handle}-resize`;
1143
+ // 핸들 위치 설정
1144
+ switch (handle) {
1145
+ case 'nw':
1146
+ handleDiv.style.top = '-5px';
1147
+ handleDiv.style.left = '-5px';
1148
+ break;
1149
+ case 'n':
1150
+ handleDiv.style.top = '-5px';
1151
+ handleDiv.style.left = '50%';
1152
+ handleDiv.style.transform = 'translateX(-50%)';
1153
+ break;
1154
+ case 'ne':
1155
+ handleDiv.style.top = '-5px';
1156
+ handleDiv.style.right = '-5px';
1157
+ break;
1158
+ case 'e':
1159
+ handleDiv.style.top = '50%';
1160
+ handleDiv.style.right = '-5px';
1161
+ handleDiv.style.transform = 'translateY(-50%)';
1162
+ break;
1163
+ case 'se':
1164
+ handleDiv.style.bottom = '-5px';
1165
+ handleDiv.style.right = '-5px';
1166
+ break;
1167
+ case 's':
1168
+ handleDiv.style.bottom = '-5px';
1169
+ handleDiv.style.left = '50%';
1170
+ handleDiv.style.transform = 'translateX(-50%)';
1171
+ break;
1172
+ case 'sw':
1173
+ handleDiv.style.bottom = '-5px';
1174
+ handleDiv.style.left = '-5px';
1175
+ break;
1176
+ case 'w':
1177
+ handleDiv.style.top = '50%';
1178
+ handleDiv.style.left = '-5px';
1179
+ handleDiv.style.transform = 'translateY(-50%)';
1180
+ break;
1181
+ }
1182
+ // 리사이즈 이벤트 핸들러
1183
+ handleDiv.onmousedown = (e) => {
1184
+ e.preventDefault();
1185
+ e.stopPropagation();
1186
+ startYoutubeResize(e, youtubeContainer, handle);
1187
+ };
1188
+ wrapper.appendChild(handleDiv);
1189
+ });
1190
+ // 유튜브를 wrapper로 감싸기
1191
+ const parent = youtubeContainer.parentNode;
1192
+ parent?.insertBefore(wrapper, youtubeContainer);
1193
+ wrapper.appendChild(youtubeContainer);
1194
+ // 편집 팝업 데이터 설정
1195
+ // 컨테이너 크기 확인
1196
+ if (youtubeContainer.style.width) {
1197
+ if (youtubeContainer.style.width === '560px') {
1198
+ setEditYoutubeWidth('original');
1199
+ }
1200
+ else if (youtubeContainer.style.width.includes('%')) {
1201
+ setEditYoutubeWidth(youtubeContainer.style.width);
1202
+ }
1203
+ else {
1204
+ // px 값을 %로 변환
1205
+ const parentWidth = editorRef.current?.offsetWidth || window.innerWidth;
1206
+ const containerWidth = parseInt(youtubeContainer.style.width);
1207
+ const percentage = Math.round((containerWidth / parentWidth) * 100);
1208
+ if (percentage >= 95) {
1209
+ setEditYoutubeWidth('100%');
1210
+ }
1211
+ else if (percentage >= 70 && percentage <= 80) {
1212
+ setEditYoutubeWidth('75%');
1213
+ }
1214
+ else if (percentage >= 45 && percentage <= 55) {
1215
+ setEditYoutubeWidth('50%');
1216
+ }
1217
+ else {
1218
+ setEditYoutubeWidth(`${percentage}%`);
1219
+ }
1220
+ }
1221
+ }
1222
+ else {
1223
+ setEditYoutubeWidth('100%');
1224
+ }
1225
+ // 정렬 확인
1226
+ let alignContainer = youtubeContainer.parentElement;
1227
+ let currentAlign = 'center';
1228
+ while (alignContainer && alignContainer !== editorRef.current) {
1229
+ if (alignContainer.tagName === 'DIV' && alignContainer.style.textAlign) {
1230
+ currentAlign = alignContainer.style.textAlign;
1231
+ break;
1232
+ }
1233
+ alignContainer = alignContainer.parentElement;
1234
+ }
1235
+ setEditYoutubeAlign(currentAlign);
1236
+ // 약간의 지연 후 편집창 열기
1237
+ setTimeout(() => {
1238
+ setIsYoutubeEditPopupOpen(true);
1239
+ }, 50);
1240
+ };
1241
+ // YouTube 선택 해제
1242
+ const deselectYoutube = () => {
1243
+ if (!selectedYoutube)
1244
+ return;
1245
+ // 원본 스타일 복원
1246
+ if (selectedYoutube.dataset.originalWidth !== undefined) {
1247
+ selectedYoutube.style.width = selectedYoutube.dataset.originalWidth;
1248
+ delete selectedYoutube.dataset.originalWidth;
1249
+ }
1250
+ if (selectedYoutube.dataset.originalDisplay !== undefined) {
1251
+ selectedYoutube.style.display = selectedYoutube.dataset.originalDisplay;
1252
+ delete selectedYoutube.dataset.originalDisplay;
1253
+ }
1254
+ // wrapper 제거
1255
+ const wrapper = selectedYoutube.parentElement;
1256
+ if (wrapper && wrapper.classList.contains('youtube-wrapper')) {
1257
+ const parent = wrapper.parentNode;
1258
+ if (parent) {
1259
+ try {
1260
+ parent.insertBefore(selectedYoutube, wrapper);
1261
+ wrapper.remove();
1262
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1263
+ }
1264
+ catch (e) {
1265
+ // 이미 제거된 경우 무시
1266
+ }
1267
+ }
1268
+ }
1269
+ // 상태 초기화
1270
+ setSelectedYoutube(null);
1271
+ setIsYoutubeEditPopupOpen(false);
1272
+ };
1273
+ // YouTube 리사이즈 시작
1274
+ const startYoutubeResize = (e, container, handle) => {
1275
+ // 컨테이너의 실제 크기를 가져옴 (getBoundingClientRect로 실제 픽셀 크기 가져오기)
1276
+ const rect = container.getBoundingClientRect();
1277
+ const currentWidth = rect.width;
1278
+ const currentHeight = rect.height || (currentWidth / (16 / 9)); // height가 없으면 16:9 비율로 계산
1279
+ setIsResizing(true);
1280
+ setResizeStartData({
1281
+ startX: e.clientX,
1282
+ startY: e.clientY,
1283
+ startWidth: currentWidth,
1284
+ startHeight: currentHeight,
1285
+ handle
1286
+ });
1287
+ };
1288
+ // YouTube 편집 적용
1289
+ const applyYoutubeEdit = () => {
1290
+ if (!selectedYoutube)
1291
+ return;
1292
+ // 크기 적용
1293
+ if (editYoutubeWidth === '100%' || editYoutubeWidth === '75%' || editYoutubeWidth === '50%') {
1294
+ // 퍼센트 값은 그대로 유지
1295
+ selectedYoutube.style.width = editYoutubeWidth;
1296
+ selectedYoutube.style.aspectRatio = '16 / 9';
1297
+ selectedYoutube.style.height = 'auto';
1298
+ }
1299
+ else if (editYoutubeWidth === 'original') {
1300
+ // original은 고정 크기
1301
+ selectedYoutube.style.aspectRatio = '';
1302
+ selectedYoutube.style.width = '560px';
1303
+ selectedYoutube.style.height = '315px';
1304
+ }
1305
+ else {
1306
+ // px 값은 그대로 설정 (리사이즈로 변경된 경우)
1307
+ selectedYoutube.style.aspectRatio = '';
1308
+ selectedYoutube.style.width = editYoutubeWidth;
1309
+ // height 계산
1310
+ const width = parseInt(editYoutubeWidth);
1311
+ const height = width / (16 / 9);
1312
+ selectedYoutube.style.height = height + 'px';
1313
+ }
1314
+ // wrapper 크기도 업데이트
1315
+ const wrapper = selectedYoutube.parentElement;
1316
+ if (wrapper && wrapper.classList.contains('youtube-wrapper')) {
1317
+ wrapper.style.width = selectedYoutube.style.width;
1318
+ wrapper.style.aspectRatio = selectedYoutube.style.aspectRatio;
1319
+ if (selectedYoutube.style.height && selectedYoutube.style.height !== 'auto') {
1320
+ wrapper.style.height = selectedYoutube.style.height;
1321
+ }
1322
+ else {
1323
+ wrapper.style.height = 'auto';
1324
+ }
1325
+ }
1326
+ // dataset의 originalWidth도 업데이트 (선택 해제 시 이 크기로 복원)
1327
+ selectedYoutube.dataset.originalWidth = selectedYoutube.style.width;
1328
+ // 정렬 적용
1329
+ // youtube-wrapper의 부모를 찾음
1330
+ const targetElement = selectedYoutube.parentElement?.classList.contains('youtube-wrapper')
1331
+ ? selectedYoutube.parentElement
1332
+ : selectedYoutube;
1333
+ // 정렬 컨테이너 찾기 (최상위 DIV 컨테이너)
1334
+ const alignContainer = targetElement?.parentElement;
1335
+ // 정렬 컨테이너가 있고 DIV이면 정렬 적용
1336
+ if (alignContainer && alignContainer.tagName === 'DIV' && alignContainer !== editorRef.current) {
1337
+ alignContainer.style.textAlign = editYoutubeAlign;
1338
+ // 유튜브 컨테이너 자체도 적절한 display 설정
1339
+ if (editYoutubeAlign === 'center' || editYoutubeAlign === 'right') {
1340
+ selectedYoutube.style.display = 'inline-block';
1341
+ }
1342
+ else {
1343
+ selectedYoutube.style.display = 'inline-block';
1344
+ }
1345
+ }
1346
+ // 선택 해제
1347
+ deselectYoutube();
1348
+ handleInput();
1349
+ };
1350
+ // YouTube 삭제
1351
+ const deleteYoutube = () => {
1352
+ if (!selectedYoutube)
1353
+ return;
1354
+ const youtubeToDelete = selectedYoutube;
1355
+ deselectYoutube();
1356
+ let elementToRemove = youtubeToDelete;
1357
+ let parent = youtubeToDelete.parentElement;
1358
+ while (parent && parent !== editorRef.current) {
1359
+ if (parent.classList.contains('youtube-wrapper') ||
1360
+ parent.classList.contains('youtube-container') ||
1361
+ (parent.tagName === 'DIV' && parent.style.textAlign)) {
1362
+ elementToRemove = parent;
1363
+ parent = parent.parentElement;
1364
+ }
1365
+ else {
1366
+ break;
1367
+ }
1368
+ }
1369
+ if (elementToRemove.parentNode) {
1370
+ elementToRemove.parentNode.removeChild(elementToRemove);
1371
+ }
1372
+ handleInput();
1373
+ };
1374
+ // YouTube 삽입
1375
+ const insertYoutube = () => {
1376
+ if (!youtubeUrl)
1377
+ return;
1378
+ const videoId = extractYoutubeVideoId(youtubeUrl);
1379
+ if (!videoId) {
1380
+ alert('올바른 유튜브 URL을 입력해주세요.\n\n지원 형식:\n• https://www.youtube.com/watch?v=VIDEO_ID\n• https://youtu.be/VIDEO_ID');
1381
+ return;
1382
+ }
1383
+ // YouTube 정렬 컨테이너 생성
1384
+ const alignContainer = document.createElement('div');
1385
+ alignContainer.style.textAlign = youtubeAlign;
1386
+ alignContainer.style.margin = '20px 0';
1387
+ // YouTube iframe 컨테이너 생성
1388
+ const container = document.createElement('div');
1389
+ container.className = 'youtube-container';
1390
+ container.style.position = 'relative';
1391
+ container.style.display = 'inline-block';
1392
+ container.style.maxWidth = '100%';
1393
+ // 크기 설정
1394
+ if (youtubeWidth === 'original') {
1395
+ container.style.width = '560px';
1396
+ container.style.height = '315px';
1397
+ }
1398
+ else if (youtubeWidth === '100%' || youtubeWidth === '75%' || youtubeWidth === '50%') {
1399
+ // 퍼센트는 그대로 유지하고 aspect-ratio 사용
1400
+ container.style.width = youtubeWidth;
1401
+ container.style.aspectRatio = '16 / 9';
1402
+ }
1403
+ else {
1404
+ // 기타 값은 그대로 설정
1405
+ container.style.width = youtubeWidth;
1406
+ container.style.aspectRatio = '16 / 9';
1407
+ }
1408
+ // iframe 생성
1409
+ const iframe = document.createElement('iframe');
1410
+ iframe.width = '100%';
1411
+ iframe.height = '100%';
1412
+ iframe.src = `https://www.youtube.com/embed/${videoId}`;
1413
+ iframe.title = 'YouTube video player';
1414
+ iframe.frameBorder = '0';
1415
+ iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share';
1416
+ iframe.allowFullscreen = true;
1417
+ iframe.style.width = '100%';
1418
+ iframe.style.height = 'auto';
1419
+ iframe.style.aspectRatio = '16 / 9';
1420
+ iframe.style.display = 'block';
1421
+ // 투명 오버레이 추가 (편집 모드에서 클릭 방지)
1422
+ const overlay = document.createElement('div');
1423
+ overlay.className = 'youtube-overlay';
1424
+ overlay.style.position = 'absolute';
1425
+ overlay.style.top = '0';
1426
+ overlay.style.left = '0';
1427
+ overlay.style.width = '100%';
1428
+ overlay.style.height = '100%';
1429
+ overlay.style.backgroundColor = 'transparent';
1430
+ overlay.style.cursor = 'pointer';
1431
+ overlay.style.zIndex = '1';
1432
+ container.appendChild(iframe);
1433
+ container.appendChild(overlay);
1434
+ alignContainer.appendChild(container);
1435
+ // 에디터에 포커스 설정
1436
+ if (editorRef.current) {
1437
+ editorRef.current.focus();
1438
+ const selection = window.getSelection();
1439
+ // 저장된 선택 영역이 있으면 복원
1440
+ if (savedYoutubeSelection && selection) {
1441
+ try {
1442
+ selection.removeAllRanges();
1443
+ selection.addRange(savedYoutubeSelection);
1444
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1445
+ }
1446
+ catch (e) {
1447
+ // 선택 영역 복원 실패 시 무시
1448
+ }
1449
+ }
1450
+ // 선택 영역 재확인
1451
+ if (!selection || selection.rangeCount === 0 || !editorRef.current.contains(selection.anchorNode)) {
1452
+ // 에디터가 비어있으면 p 태그 추가
1453
+ if (!editorRef.current.innerHTML || editorRef.current.innerHTML === '<br>') {
1454
+ const p = document.createElement('p');
1455
+ p.innerHTML = '<br>';
1456
+ editorRef.current.appendChild(p);
1457
+ }
1458
+ // 커서를 에디터 끝으로 이동
1459
+ const range = document.createRange();
1460
+ range.selectNodeContents(editorRef.current);
1461
+ range.collapse(false);
1462
+ selection?.removeAllRanges();
1463
+ selection?.addRange(range);
1464
+ }
1465
+ // YouTube iframe 삽입
1466
+ if (selection && selection.rangeCount > 0) {
1467
+ const range = selection.getRangeAt(0);
1468
+ range.deleteContents();
1469
+ range.insertNode(alignContainer);
1470
+ // iframe 다음에 새 문단 추가
1471
+ const newP = document.createElement('p');
1472
+ newP.innerHTML = '<br>';
1473
+ alignContainer.after(newP);
1474
+ // 커서를 새 문단으로 이동
1475
+ const newRange = document.createRange();
1476
+ newRange.selectNodeContents(newP);
1477
+ newRange.collapse(true);
1478
+ selection.removeAllRanges();
1479
+ selection.addRange(newRange);
1480
+ }
1481
+ else {
1482
+ // 폴백: 에디터 끝에 추가
1483
+ editorRef.current.appendChild(alignContainer);
1484
+ }
1485
+ }
1486
+ // 상태 초기화
1487
+ setIsYoutubeDropdownOpen(false);
1488
+ setYoutubeUrl('');
1489
+ setYoutubeWidth('100%'); // 초기화
1490
+ setYoutubeAlign('center'); // 초기화
1491
+ setSavedYoutubeSelection(null);
1492
+ editorRef.current?.focus();
1493
+ handleInput();
1494
+ };
1495
+ const handleImageFileSelect = (e) => {
1496
+ const file = e.target.files?.[0];
1497
+ if (file && file.type.startsWith('image/')) {
1498
+ setImageFile(file);
1499
+ const reader = new FileReader();
1500
+ reader.onloadend = () => {
1501
+ const base64String = reader.result;
1502
+ setImagePreview(base64String);
1503
+ // URL 필드 초기화
1504
+ setImageUrl('');
1505
+ };
1506
+ reader.readAsDataURL(file);
1507
+ }
1508
+ else {
1509
+ alert('이미지 파일을 선택해주세요.');
1510
+ }
1511
+ // 같은 파일을 다시 선택할 수 있도록 초기화
1512
+ e.target.value = '';
1513
+ };
1514
+ // 기존 handleFileUpload은 삭제 또는 제거 예정
1515
+ const handleFileUpload = (e) => {
1516
+ const file = e.target.files?.[0];
1517
+ if (file && file.type.startsWith('image/')) {
1518
+ const reader = new FileReader();
1519
+ reader.onloadend = () => {
1520
+ const base64String = reader.result;
1521
+ execCommand('insertImage', base64String);
1522
+ };
1523
+ reader.readAsDataURL(file);
1524
+ }
1525
+ else {
1526
+ alert('이미지 파일을 선택해주세요.');
1527
+ }
1528
+ // 같은 파일을 다시 선택할 수 있도록 초기화
1529
+ e.target.value = '';
1530
+ };
1531
+ const handleKeyDown = (e) => {
1532
+ // Backspace 또는 Delete 키로 선택된 이미지 삭제
1533
+ if ((e.key === 'Backspace' || e.key === 'Delete') && selectedImage) {
1534
+ e.preventDefault();
1535
+ // deleteImage 함수 호출로 통합
1536
+ deleteImage();
1537
+ return;
1538
+ }
1539
+ // Backspace 또는 Delete 키로 선택된 유튜브 삭제
1540
+ if ((e.key === 'Backspace' || e.key === 'Delete') && selectedYoutube) {
1541
+ e.preventDefault();
1542
+ deleteYoutube();
1543
+ return;
1544
+ }
1545
+ // 에디터가 비어있고 처음 입력하는 경우
1546
+ if (editorRef.current && (!editorRef.current.innerHTML || editorRef.current.innerHTML === '<br>')) {
1547
+ // Enter, Backspace, Delete가 아닌 일반 문자 입력인 경우
1548
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
1549
+ e.preventDefault();
1550
+ // p 태그 생성 및 텍스트 삽입
1551
+ const p = document.createElement('p');
1552
+ p.textContent = e.key;
1553
+ editorRef.current.innerHTML = '';
1554
+ editorRef.current.appendChild(p);
1555
+ // 커서를 텍스트 끝으로 이동
1556
+ const selection = window.getSelection();
1557
+ const range = document.createRange();
1558
+ range.selectNodeContents(p);
1559
+ range.collapse(false);
1560
+ selection?.removeAllRanges();
1561
+ selection?.addRange(range);
1562
+ handleInput();
1563
+ return;
1564
+ }
1565
+ }
1566
+ if (e.key === 'Enter' && !e.shiftKey) {
1567
+ // Enter만 눌렀을 때: p 태그로 새 문단 생성
1568
+ e.preventDefault();
1569
+ // insertParagraph를 사용하여 새 문단 생성
1570
+ document.execCommand('insertParagraph', false);
1571
+ // 새로 생성된 문단을 p 태그로 변환
1572
+ setTimeout(() => {
1573
+ const selection = window.getSelection();
1574
+ if (selection && selection.rangeCount > 0) {
1575
+ const range = selection.getRangeAt(0);
1576
+ let container = range.commonAncestorContainer;
1577
+ // 텍스트 노드인 경우 부모 요소로
1578
+ if (container.nodeType === Node.TEXT_NODE) {
1579
+ container = container.parentElement;
1580
+ }
1581
+ // div인 경우 p로 변경
1582
+ if (container && container.tagName === 'DIV') {
1583
+ document.execCommand('formatBlock', false, 'p');
1584
+ }
1585
+ }
1586
+ handleInput();
1587
+ }, 0);
1588
+ }
1589
+ // Shift+Enter는 브라우저 기본 동작 사용 (br 태그 삽입)
1590
+ };
1591
+ // 선택 영역 저장
1592
+ const saveSelection = () => {
1593
+ const selection = window.getSelection();
1594
+ if (selection && selection.rangeCount > 0) {
1595
+ return selection.getRangeAt(0);
1596
+ }
1597
+ return null;
1598
+ };
1599
+ // 선택 영역 복원
1600
+ const restoreSelection = (range) => {
1601
+ if (range) {
1602
+ const selection = window.getSelection();
1603
+ if (selection) {
1604
+ selection.removeAllRanges();
1605
+ selection.addRange(range);
1606
+ }
1607
+ }
1608
+ };
1609
+ const applyColorStyle = (styleProperty, color, savedRange) => {
1610
+ // 저장된 선택 영역이 있으면 복원
1611
+ if (savedRange) {
1612
+ restoreSelection(savedRange);
1613
+ }
1614
+ const selection = window.getSelection();
1615
+ if (!selection || selection.isCollapsed) {
1616
+ return;
1617
+ }
1618
+ const range = selection.getRangeAt(0);
1619
+ // 선택된 텍스트를 span으로 감싸기
1620
+ const span = document.createElement('span');
1621
+ try {
1622
+ const contents = range.extractContents();
1623
+ // 스타일 적용 - setAttribute를 사용하여 !important 포함
1624
+ if (styleProperty === 'color') {
1625
+ span.setAttribute('style', `color: ${color} !important;`);
1626
+ }
1627
+ else if (styleProperty === 'background-color') {
1628
+ span.setAttribute('style', `background-color: ${color} !important;`);
1629
+ }
1630
+ span.appendChild(contents);
1631
+ range.insertNode(span);
1632
+ // 커서 위치 조정
1633
+ range.selectNodeContents(span);
1634
+ range.collapse(false);
1635
+ selection.removeAllRanges();
1636
+ selection.addRange(range);
1637
+ }
1638
+ catch {
1639
+ // 폴백: execCommand 사용
1640
+ if (styleProperty === 'color') {
1641
+ document.execCommand('foreColor', false, color);
1642
+ }
1643
+ else {
1644
+ document.execCommand('hiliteColor', false, color);
1645
+ }
1646
+ }
1647
+ editorRef.current?.focus();
1648
+ handleInput();
1649
+ };
1650
+ const changeFontColor = (color, savedRange) => {
1651
+ applyColorStyle('color', color, savedRange);
1652
+ };
1653
+ const changeBackgroundColor = (color, savedRange) => {
1654
+ applyColorStyle('background-color', color, savedRange);
1655
+ };
1656
+ // 클라이언트에서만 고유 ID 생성
1657
+ useEffect(() => {
1658
+ setEditorID(`editor-${uuid()}`);
1659
+ }, []);
1660
+ useEffect(() => {
1661
+ if (editorRef.current && value && !editorRef.current.innerHTML) {
1662
+ editorRef.current.innerHTML = value;
1663
+ }
1664
+ }, [value]);
1665
+ // 외부 클릭 시 드롭다운 닫기
1666
+ useEffect(() => {
1667
+ const handleClickOutside = (event) => {
1668
+ const target = event.target;
1669
+ if (paragraphButtonRef.current && !paragraphButtonRef.current.contains(target)) {
1670
+ setIsParagraphDropdownOpen(false);
1671
+ }
1672
+ if (textColorButtonRef.current && !textColorButtonRef.current.contains(target)) {
1673
+ setIsTextColorOpen(false);
1674
+ }
1675
+ if (bgColorButtonRef.current && !bgColorButtonRef.current.contains(target)) {
1676
+ setIsBgColorOpen(false);
1677
+ }
1678
+ if (alignButtonRef.current && !alignButtonRef.current.contains(target)) {
1679
+ setIsAlignDropdownOpen(false);
1680
+ }
1681
+ if (linkButtonRef.current && !linkButtonRef.current.contains(target)) {
1682
+ setIsLinkDropdownOpen(false);
1683
+ setLinkUrl('');
1684
+ setLinkTarget('_blank');
1685
+ setSavedSelection(null);
1686
+ }
1687
+ // 이미지 드롭다운 체크 - 드롭다운 자체도 체크
1688
+ const imageDropdown = document.querySelector(`.${styles.imageDropdown}`);
1689
+ if (imageButtonRef.current &&
1690
+ !imageButtonRef.current.contains(target) &&
1691
+ (!imageDropdown || !imageDropdown.contains(target))) {
1692
+ setIsImageDropdownOpen(false);
1693
+ setImageTabMode('file'); // 탭 모드 초기화
1694
+ setImageUrl('');
1695
+ setImageFile(null);
1696
+ setImagePreview('');
1697
+ setImageWidth('original'); // 원본으로 초기화
1698
+ setImageAlign('left'); // 좌측으로 초기화
1699
+ setImageAlt('');
1700
+ setSavedImageSelection(null); // 저장된 선택 영역 초기화
1701
+ }
1702
+ // 유튜브 드롭다운 체크
1703
+ const youtubeDropdown = document.querySelector(`.${styles.youtubeDropdown}`);
1704
+ if (youtubeButtonRef.current &&
1705
+ !youtubeButtonRef.current.contains(target) &&
1706
+ (!youtubeDropdown || !youtubeDropdown.contains(target))) {
1707
+ setIsYoutubeDropdownOpen(false);
1708
+ setYoutubeUrl('');
1709
+ setSavedYoutubeSelection(null);
1710
+ }
1711
+ // 이미지 편집 팝업 닫기
1712
+ if (isImageEditPopupOpen && selectedImage) {
1713
+ const imageEditPopup = document.querySelector(`.${styles.imageDropdown}`);
1714
+ // 편집 팝업, 선택된 이미지, image-wrapper 외부를 클릭한 경우
1715
+ if (imageEditPopup &&
1716
+ !imageEditPopup.contains(target) &&
1717
+ !selectedImage.contains(target) &&
1718
+ !selectedImage.parentElement?.contains(target)) {
1719
+ setIsImageEditPopupOpen(false);
1720
+ }
1721
+ }
1722
+ // 링크 수정 팝업 닫기
1723
+ if (isEditLinkPopupOpen) {
1724
+ const editPopup = document.querySelector(`.${styles.editLinkPopup}`);
1725
+ if (editPopup && !editPopup.contains(target) && !selectedLinkElement?.contains(target)) {
1726
+ setIsEditLinkPopupOpen(false);
1727
+ setSelectedLinkElement(null);
1728
+ setEditLinkUrl('');
1729
+ setEditLinkTarget('_self');
1730
+ }
1731
+ }
1732
+ };
1733
+ if (isParagraphDropdownOpen || isTextColorOpen || isBgColorOpen || isAlignDropdownOpen || isLinkDropdownOpen || isEditLinkPopupOpen || isImageDropdownOpen || isImageEditPopupOpen || isYoutubeDropdownOpen) {
1734
+ document.addEventListener('mousedown', handleClickOutside);
1735
+ }
1736
+ return () => {
1737
+ document.removeEventListener('mousedown', handleClickOutside);
1738
+ };
1739
+ }, [isParagraphDropdownOpen, isTextColorOpen, isBgColorOpen, isAlignDropdownOpen, isLinkDropdownOpen, isEditLinkPopupOpen, isImageDropdownOpen, isImageEditPopupOpen, isYoutubeDropdownOpen, selectedLinkElement, selectedImage]);
1740
+ // 리사이즈 중 마우스 이벤트 처리
1741
+ useEffect(() => {
1742
+ if (!isResizing || !resizeStartData)
1743
+ return;
1744
+ const handleMouseMove = (e) => {
1745
+ if (!resizeStartData)
1746
+ return;
1747
+ const deltaX = e.clientX - resizeStartData.startX;
1748
+ const deltaY = e.clientY - resizeStartData.startY;
1749
+ // 이미지 리사이즈
1750
+ if (selectedImage) {
1751
+ const aspectRatio = resizeStartData.startWidth / resizeStartData.startHeight;
1752
+ let newWidth = resizeStartData.startWidth;
1753
+ let newHeight = resizeStartData.startHeight;
1754
+ switch (resizeStartData.handle) {
1755
+ case 'e':
1756
+ case 'w':
1757
+ newWidth = resizeStartData.startWidth + (resizeStartData.handle === 'e' ? deltaX : -deltaX);
1758
+ newHeight = newWidth / aspectRatio;
1759
+ break;
1760
+ case 'n':
1761
+ case 's':
1762
+ newHeight = resizeStartData.startHeight + (resizeStartData.handle === 's' ? deltaY : -deltaY);
1763
+ newWidth = newHeight * aspectRatio;
1764
+ break;
1765
+ case 'ne':
1766
+ case 'nw':
1767
+ case 'se':
1768
+ case 'sw': {
1769
+ // 대각선 리사이즈는 더 큰 변화량 기준
1770
+ const diagonalDelta = Math.abs(deltaX) > Math.abs(deltaY) ? deltaX : deltaY;
1771
+ const multiplier = resizeStartData.handle.includes('e') ? 1 : -1;
1772
+ newWidth = resizeStartData.startWidth + (diagonalDelta * multiplier);
1773
+ newHeight = newWidth / aspectRatio;
1774
+ break;
1775
+ }
1776
+ }
1777
+ // 최소 크기 제한
1778
+ newWidth = Math.max(50, newWidth);
1779
+ newHeight = Math.max(50, newHeight);
1780
+ selectedImage.style.width = newWidth + 'px';
1781
+ selectedImage.style.height = newHeight + 'px';
1782
+ }
1783
+ // 유튜브 리사이즈
1784
+ if (selectedYoutube) {
1785
+ const aspectRatio = 16 / 9; // 유튜브는 16:9 고정
1786
+ let newWidth = resizeStartData.startWidth;
1787
+ let newHeight = resizeStartData.startHeight;
1788
+ switch (resizeStartData.handle) {
1789
+ case 'e':
1790
+ case 'w':
1791
+ newWidth = resizeStartData.startWidth + (resizeStartData.handle === 'e' ? deltaX : -deltaX);
1792
+ newHeight = newWidth / aspectRatio;
1793
+ break;
1794
+ case 'n':
1795
+ case 's':
1796
+ newHeight = resizeStartData.startHeight + (resizeStartData.handle === 's' ? deltaY : -deltaY);
1797
+ newWidth = newHeight * aspectRatio;
1798
+ break;
1799
+ case 'ne':
1800
+ case 'nw':
1801
+ case 'se':
1802
+ case 'sw': {
1803
+ // 대각선 리사이즈는 더 큰 변화량 기준
1804
+ const diagonalDelta = Math.abs(deltaX) > Math.abs(deltaY) ? deltaX : deltaY;
1805
+ const multiplier = resizeStartData.handle.includes('e') ? 1 : -1;
1806
+ newWidth = resizeStartData.startWidth + (diagonalDelta * multiplier);
1807
+ newHeight = newWidth / aspectRatio;
1808
+ break;
1809
+ }
1810
+ }
1811
+ // 최소/최대 크기 제한
1812
+ const parentWidth = editorRef.current?.offsetWidth || window.innerWidth;
1813
+ newWidth = Math.max(200, Math.min(newWidth, parentWidth - 40));
1814
+ newHeight = newWidth / aspectRatio;
1815
+ // 유튜브 컨테이너 크기 업데이트
1816
+ // aspectRatio를 제거하고 명시적인 크기 설정
1817
+ selectedYoutube.style.aspectRatio = '';
1818
+ selectedYoutube.style.width = newWidth + 'px';
1819
+ selectedYoutube.style.height = newHeight + 'px';
1820
+ // wrapper 크기도 업데이트
1821
+ const wrapper = selectedYoutube.parentElement;
1822
+ if (wrapper && wrapper.classList.contains('youtube-wrapper')) {
1823
+ wrapper.style.width = newWidth + 'px';
1824
+ wrapper.style.height = newHeight + 'px';
1825
+ }
1826
+ // 편집 중인 크기 업데이트
1827
+ const percentage = Math.round((newWidth / parentWidth) * 100);
1828
+ if (percentage >= 95) {
1829
+ setEditYoutubeWidth('100%');
1830
+ }
1831
+ else if (percentage >= 70 && percentage <= 80) {
1832
+ setEditYoutubeWidth('75%');
1833
+ }
1834
+ else if (percentage >= 45 && percentage <= 55) {
1835
+ setEditYoutubeWidth('50%');
1836
+ }
1837
+ else {
1838
+ setEditYoutubeWidth(`${percentage}%`);
1839
+ }
1840
+ }
1841
+ };
1842
+ const handleMouseUp = () => {
1843
+ setIsResizing(false);
1844
+ setResizeStartData(null);
1845
+ if (selectedImage) {
1846
+ setEditImageWidth(selectedImage.style.width);
1847
+ }
1848
+ if (selectedYoutube) {
1849
+ // 유튜브 크기는 이미 px로 설정되어 있으므로,
1850
+ // 편집창의 width 값만 업데이트 (실제 DOM은 변경하지 않음)
1851
+ const currentWidth = selectedYoutube.style.width;
1852
+ setEditYoutubeWidth(currentWidth);
1853
+ // 변경된 크기를 새로운 원본으로 설정 (선택 해제 시 이 크기로 복원됨)
1854
+ selectedYoutube.dataset.originalWidth = currentWidth;
1855
+ }
1856
+ };
1857
+ document.addEventListener('mousemove', handleMouseMove);
1858
+ document.addEventListener('mouseup', handleMouseUp);
1859
+ return () => {
1860
+ document.removeEventListener('mousemove', handleMouseMove);
1861
+ document.removeEventListener('mouseup', handleMouseUp);
1862
+ };
1863
+ }, [isResizing, resizeStartData, selectedImage, selectedYoutube]);
1864
+ // 스크롤, 리사이즈 및 이미지/유튜브 드래그 시 편집창 숨기기
1865
+ useEffect(() => {
1866
+ if (!selectedImage && !selectedYoutube)
1867
+ return;
1868
+ // 스크롤 이벤트 핸들러
1869
+ const handleScroll = () => {
1870
+ if (isImageEditPopupOpen) {
1871
+ setIsImageEditPopupOpen(false);
1872
+ }
1873
+ if (isYoutubeEditPopupOpen) {
1874
+ setIsYoutubeEditPopupOpen(false);
1875
+ }
1876
+ };
1877
+ // 리사이즈 이벤트 핸들러
1878
+ const handleResize = () => {
1879
+ if (isImageEditPopupOpen) {
1880
+ setIsImageEditPopupOpen(false);
1881
+ }
1882
+ if (isYoutubeEditPopupOpen) {
1883
+ setIsYoutubeEditPopupOpen(false);
1884
+ }
1885
+ };
1886
+ // 드래그 시작 이벤트 핸들러
1887
+ const handleDragStart = (e) => {
1888
+ if (e.target === selectedImage && selectedImage) {
1889
+ setIsImageEditPopupOpen(false);
1890
+ // 드래그 효과를 'move'로 설정하여 복제가 아닌 이동으로 동작하도록 함
1891
+ e.dataTransfer.effectAllowed = 'move';
1892
+ e.dataTransfer.dropEffect = 'move';
1893
+ // wrapper를 숨겨서 드래그 중 핸들이 보이지 않도록 함 (DOM 조작은 dragend에서 수행)
1894
+ const wrapper = selectedImage.parentElement;
1895
+ if (wrapper && wrapper.classList.contains('image-wrapper')) {
1896
+ wrapper.style.border = 'none';
1897
+ const handles = wrapper.querySelectorAll('.resize-handle');
1898
+ handles.forEach((handle) => {
1899
+ handle.style.display = 'none';
1900
+ });
1901
+ }
1902
+ }
1903
+ if (e.target === selectedYoutube && selectedYoutube) {
1904
+ setIsYoutubeEditPopupOpen(false);
1905
+ // 드래그 효과를 'move'로 설정하여 복제가 아닌 이동으로 동작하도록 함
1906
+ e.dataTransfer.effectAllowed = 'move';
1907
+ e.dataTransfer.dropEffect = 'move';
1908
+ // wrapper를 숨겨서 드래그 중 핸들이 보이지 않도록 함 (DOM 조작은 dragend에서 수행)
1909
+ const wrapper = selectedYoutube.parentElement;
1910
+ if (wrapper && wrapper.classList.contains('youtube-wrapper')) {
1911
+ wrapper.style.border = 'none';
1912
+ const handles = wrapper.querySelectorAll('.resize-handle');
1913
+ handles.forEach((handle) => {
1914
+ handle.style.display = 'none';
1915
+ });
1916
+ }
1917
+ }
1918
+ };
1919
+ // 드래그 종료 이벤트 핸들러 - 이미지/유튜브 이동 후 선택 해제
1920
+ const handleDragEnd = () => {
1921
+ // 드래그 완료 후 wrapper를 제거하여 선택 해제
1922
+ if (selectedImage) {
1923
+ deselectImage();
1924
+ }
1925
+ if (selectedYoutube) {
1926
+ deselectYoutube();
1927
+ }
1928
+ };
1929
+ // 이벤트 리스너 등록
1930
+ window.addEventListener('scroll', handleScroll, true);
1931
+ window.addEventListener('resize', handleResize);
1932
+ editorRef.current?.addEventListener('scroll', handleScroll);
1933
+ containerRef.current?.addEventListener('resize', handleResize);
1934
+ if (selectedImage) {
1935
+ selectedImage.addEventListener('dragstart', handleDragStart);
1936
+ selectedImage.addEventListener('dragend', handleDragEnd);
1937
+ // 이미지에 draggable 속성 추가
1938
+ selectedImage.draggable = true;
1939
+ }
1940
+ if (selectedYoutube) {
1941
+ selectedYoutube.addEventListener('dragstart', handleDragStart);
1942
+ selectedYoutube.addEventListener('dragend', handleDragEnd);
1943
+ // 유튜브에 draggable 속성 추가
1944
+ selectedYoutube.draggable = true;
1945
+ }
1946
+ return () => {
1947
+ window.removeEventListener('scroll', handleScroll, true);
1948
+ window.removeEventListener('resize', handleResize);
1949
+ editorRef.current?.removeEventListener('scroll', handleScroll);
1950
+ containerRef.current?.removeEventListener('resize', handleResize);
1951
+ if (selectedImage) {
1952
+ selectedImage.removeEventListener('dragstart', handleDragStart);
1953
+ selectedImage.removeEventListener('dragend', handleDragEnd);
1954
+ selectedImage.draggable = false;
1955
+ }
1956
+ if (selectedYoutube) {
1957
+ selectedYoutube.removeEventListener('dragstart', handleDragStart);
1958
+ selectedYoutube.removeEventListener('dragend', handleDragEnd);
1959
+ selectedYoutube.draggable = false;
1960
+ }
1961
+ };
1962
+ }, [selectedImage, selectedYoutube, isImageEditPopupOpen, isYoutubeEditPopupOpen]);
1963
+ // DOM Mutation Observer - 선택된 이미지가 DOM에서 제거되는 것을 감지
1964
+ useEffect(() => {
1965
+ if (!selectedImage || !editorRef.current)
1966
+ return;
1967
+ const observer = new MutationObserver((mutations) => {
1968
+ mutations.forEach((mutation) => {
1969
+ // 제거된 노드들 확인
1970
+ mutation.removedNodes.forEach((node) => {
1971
+ // 제거된 노드가 선택된 이미지이거나 그것을 포함하는 경우
1972
+ if (node === selectedImage ||
1973
+ (node.nodeType === Node.ELEMENT_NODE &&
1974
+ node.contains(selectedImage))) {
1975
+ // 선택 상태 해제
1976
+ deselectImage();
1977
+ }
1978
+ });
1979
+ });
1980
+ });
1981
+ // 에디터 관찰 시작
1982
+ observer.observe(editorRef.current, {
1983
+ childList: true,
1984
+ subtree: true
1985
+ });
1986
+ return () => {
1987
+ observer.disconnect();
1988
+ };
1989
+ }, [selectedImage]);
1990
+ // DOM Mutation Observer - 선택된 유튜브가 DOM에서 제거되는 것을 감지
1991
+ useEffect(() => {
1992
+ if (!selectedYoutube || !editorRef.current)
1993
+ return;
1994
+ const observer = new MutationObserver((mutations) => {
1995
+ mutations.forEach((mutation) => {
1996
+ // 제거된 노드들 확인
1997
+ mutation.removedNodes.forEach((node) => {
1998
+ // 제거된 노드가 선택된 유튜브이거나 그것을 포함하는 경우
1999
+ if (node === selectedYoutube ||
2000
+ (node.nodeType === Node.ELEMENT_NODE &&
2001
+ node.contains(selectedYoutube))) {
2002
+ // 선택 상태 해제
2003
+ deselectYoutube();
2004
+ }
2005
+ });
2006
+ });
2007
+ });
2008
+ // 에디터 관찰 시작
2009
+ observer.observe(editorRef.current, {
2010
+ childList: true,
2011
+ subtree: true
2012
+ });
2013
+ return () => {
2014
+ observer.disconnect();
2015
+ };
2016
+ }, [selectedYoutube]);
2017
+ // 외부에서 value가 변경되면 히스토리 초기화 (단, undo/redo 중에는 제외)
2018
+ useEffect(() => {
2019
+ if (isUndoRedoRef.current) {
2020
+ return;
2021
+ }
2022
+ if (editorRef.current && editorRef.current.innerHTML !== value) {
2023
+ editorRef.current.innerHTML = value;
2024
+ setHistory([value]);
2025
+ setHistoryIndex(0);
2026
+ }
2027
+ }, [value]);
2028
+ // 초기 로드 시 문단 형식 감지 (기본 p 태그는 추가하지 않음)
2029
+ useEffect(() => {
2030
+ // 약간의 지연을 주어 DOM이 완전히 렌더링된 후 감지
2031
+ const timer = setTimeout(() => {
2032
+ if (editorRef.current && editorRef.current.innerHTML) {
2033
+ // 내용이 있을 때만 문단 형식 감지
2034
+ detectCurrentParagraphStyle();
2035
+ detectCurrentAlign();
2036
+ }
2037
+ }, 100);
2038
+ return () => clearTimeout(timer);
2039
+ }, []);
2040
+ return (_jsxs("div", { className: `${styles.editor} ${statusClass}`, style: { width, position: 'relative' }, children: [_jsxs("div", { className: styles.toolbar, children: [_jsxs("div", { className: styles.toolbarGroup, children: [_jsx("button", { type: "button", className: styles.toolbarButton, onClick: () => execCommand('undo'), disabled: historyIndex <= 0, title: "\uC2E4\uD589 \uCDE8\uC18C", style: {
2041
+ opacity: historyIndex <= 0 ? 0.65 : 1,
2042
+ backgroundColor: 'transparent',
2043
+ border: 'none',
2044
+ cursor: historyIndex <= 0 ? 'not-allowed' : 'pointer'
2045
+ }, children: _jsx("i", { className: styles.undo }) }), _jsx("button", { type: "button", className: styles.toolbarButton, onClick: () => execCommand('redo'), disabled: historyIndex >= history.length - 1, title: "\uB2E4\uC2DC \uC2E4\uD589", style: {
2046
+ opacity: historyIndex >= history.length - 1 ? 0.65 : 1,
2047
+ backgroundColor: 'transparent',
2048
+ border: 'none',
2049
+ cursor: historyIndex >= history.length - 1 ? 'not-allowed' : 'pointer'
2050
+ }, children: _jsx("i", { className: styles.redo }) })] }), _jsxs("div", { className: styles.toolbarGroup, ref: paragraphButtonRef, children: [_jsxs("button", { type: "button", className: styles.paragraphButton, onClick: () => {
2051
+ setIsParagraphDropdownOpen(!isParagraphDropdownOpen);
2052
+ setIsTextColorOpen(false);
2053
+ setIsBgColorOpen(false);
2054
+ setIsAlignDropdownOpen(false);
2055
+ }, title: "\uBB38\uB2E8 \uD615\uC2DD", children: [_jsx("span", { children: getCurrentStyleLabel() }), _jsx("i", { className: styles.dropdownArrow })] }), isParagraphDropdownOpen && (_jsx("div", { className: styles.paragraphDropdown, style: {
2056
+ top: paragraphButtonRef.current?.getBoundingClientRect().bottom ?? 0,
2057
+ left: paragraphButtonRef.current?.getBoundingClientRect().left ?? 0
2058
+ }, children: paragraphOptions.map((option) => (_jsx("button", { type: "button", className: `${styles.paragraphOption} ${currentParagraphStyle === option.value ? styles.active : ''}`, onClick: () => applyParagraphStyle(option.value), children: option.value === 'h1' ? (_jsx("h1", { children: option.label })) : option.value === 'h2' ? (_jsx("h2", { children: option.label })) : option.value === 'h3' ? (_jsx("h3", { children: option.label })) : (_jsx("span", { className: option.className || '', children: option.label })) }, option.value))) }))] }), _jsxs("div", { className: styles.toolbarGroup, children: [_jsx("button", { type: "button", className: styles.toolbarButton, onClick: () => execCommand('bold'), title: "\uAD75\uAC8C", children: _jsx("i", { className: styles.bold }) }), _jsx("button", { type: "button", className: styles.toolbarButton, onClick: () => execCommand('italic'), title: "\uAE30\uC6B8\uC784", children: _jsx("i", { className: styles.italic }) }), _jsx("button", { type: "button", className: styles.toolbarButton, onClick: () => execCommand('underline'), title: "\uBC11\uC904", children: _jsx("i", { className: styles.underline }) }), _jsx("button", { type: "button", className: styles.toolbarButton, onClick: () => execCommand('strikeThrough'), title: "\uCDE8\uC18C\uC120", children: _jsx("i", { className: styles.strikethrough }) })] }), _jsxs("div", { className: styles.toolbarGroup, children: [_jsxs("div", { ref: textColorButtonRef, style: { position: 'relative' }, children: [_jsx("button", { type: "button", className: styles.toolbarButton, onClick: () => {
2059
+ const selection = window.getSelection();
2060
+ if (selection && !selection.isCollapsed) {
2061
+ // 선택 영역 저장
2062
+ const range = saveSelection();
2063
+ setSavedSelection(range);
2064
+ setIsTextColorOpen(!isTextColorOpen);
2065
+ setIsBgColorOpen(false);
2066
+ }
2067
+ }, title: "\uAE00\uAF34 \uC0C9\uC0C1", children: _jsx("i", { className: styles.fontColor }) }), isTextColorOpen && (_jsx("div", { className: styles.colorPalette, style: {
2068
+ top: textColorButtonRef.current?.getBoundingClientRect().bottom ?? 0,
2069
+ left: textColorButtonRef.current?.getBoundingClientRect().left ?? 0
2070
+ }, children: colorPalette.map((row, rowIndex) => (_jsx("div", { className: styles.colorRow, children: row.map((color) => (_jsx("button", { type: "button", className: styles.colorButton, style: { backgroundColor: color }, onMouseDown: (e) => e.preventDefault(), onClick: () => {
2071
+ changeFontColor(color, savedSelection);
2072
+ setIsTextColorOpen(false);
2073
+ setSavedSelection(null);
2074
+ } }, color))) }, rowIndex))) }))] }), _jsxs("div", { ref: bgColorButtonRef, style: { position: 'relative' }, children: [_jsx("button", { type: "button", className: styles.toolbarButton, onClick: () => {
2075
+ const selection = window.getSelection();
2076
+ if (selection && !selection.isCollapsed) {
2077
+ // 선택 영역 저장
2078
+ const range = saveSelection();
2079
+ setSavedSelection(range);
2080
+ setIsBgColorOpen(!isBgColorOpen);
2081
+ setIsTextColorOpen(false);
2082
+ }
2083
+ }, title: "\uBC30\uACBD \uC0C9\uC0C1", children: _jsx("i", { className: styles.highlight }) }), isBgColorOpen && (_jsx("div", { className: styles.colorPalette, style: {
2084
+ top: bgColorButtonRef.current?.getBoundingClientRect().bottom ?? 0,
2085
+ left: bgColorButtonRef.current?.getBoundingClientRect().left ?? 0
2086
+ }, children: colorPalette.map((row, rowIndex) => (_jsx("div", { className: styles.colorRow, children: row.map((color) => (_jsx("button", { type: "button", className: styles.colorButton, style: { backgroundColor: color }, onMouseDown: (e) => e.preventDefault(), onClick: () => {
2087
+ changeBackgroundColor(color, savedSelection);
2088
+ setIsBgColorOpen(false);
2089
+ setSavedSelection(null);
2090
+ } }, color))) }, rowIndex))) }))] })] }), _jsxs("div", { className: styles.toolbarGroup, children: [_jsxs("div", { ref: alignButtonRef, style: { position: 'relative' }, children: [_jsx("button", { type: "button", className: styles.toolbarButton, onClick: () => {
2091
+ setIsAlignDropdownOpen(!isAlignDropdownOpen);
2092
+ setIsParagraphDropdownOpen(false);
2093
+ setIsTextColorOpen(false);
2094
+ setIsBgColorOpen(false);
2095
+ }, title: getCurrentAlignLabel(), children: _jsx("i", { className: getCurrentAlignIcon() }) }), isAlignDropdownOpen && (_jsx("div", { className: styles.alignDropdown, style: {
2096
+ top: alignButtonRef.current?.getBoundingClientRect().bottom ?? 0,
2097
+ left: alignButtonRef.current?.getBoundingClientRect().left ?? 0
2098
+ }, children: alignOptions.map((option) => (_jsx("button", { type: "button", className: `${styles.alignOption} ${currentAlign === option.value ? styles.active : ''}`, onClick: () => {
2099
+ if (option.value === 'left') {
2100
+ execCommand('justifyLeft');
2101
+ }
2102
+ else if (option.value === 'center') {
2103
+ execCommand('justifyCenter');
2104
+ }
2105
+ else if (option.value === 'right') {
2106
+ execCommand('justifyRight');
2107
+ }
2108
+ setCurrentAlign(option.value);
2109
+ setIsAlignDropdownOpen(false);
2110
+ }, title: option.label, children: _jsx("i", { className: styles[option.icon] }) }, option.value))) }))] }), _jsx("button", { type: "button", className: styles.toolbarButton, onClick: () => execCommand('insertUnorderedList'), title: "\uBAA9\uB85D", children: _jsx("i", { className: styles.listUl }) }), _jsx("button", { type: "button", className: styles.toolbarButton, onClick: () => execCommand('insertOrderedList'), title: "\uBC88\uD638 \uBAA9\uB85D", children: _jsx("i", { className: styles.listOl }) })] }), _jsxs("div", { className: styles.toolbarGroup, children: [_jsxs("div", { ref: linkButtonRef, style: { position: 'relative' }, children: [_jsx("button", { type: "button", className: styles.toolbarButton, onClick: openLinkDropdown, title: "\uB9C1\uD06C", children: _jsx("i", { className: styles.link }) }), isLinkDropdownOpen && (_jsxs("div", { className: styles.linkDropdown, style: {
2111
+ top: linkButtonRef.current?.getBoundingClientRect().bottom ?? 0,
2112
+ left: linkButtonRef.current?.getBoundingClientRect().left ?? 0
2113
+ }, children: [_jsxs("div", { className: styles.linkInput, children: [_jsx("label", { children: "URL" }), _jsx("input", { type: "text", value: linkUrl, onChange: (e) => setLinkUrl(e.target.value), placeholder: "https://...", autoFocus: true })] }), _jsxs("div", { className: styles.linkTarget, children: [_jsxs("label", { children: [_jsx("input", { type: "radio", value: "_blank", checked: linkTarget === '_blank', onChange: (e) => setLinkTarget(e.target.value) }), "\uC0C8 \uCC3D\uC5D0\uC11C \uC5F4\uAE30"] }), _jsxs("label", { children: [_jsx("input", { type: "radio", value: "_self", checked: linkTarget === '_self', onChange: (e) => setLinkTarget(e.target.value) }), "\uD604\uC7AC \uCC3D\uC5D0\uC11C \uC5F4\uAE30"] })] }), _jsxs("div", { className: styles.linkActions, children: [_jsx("button", { type: "button", onClick: () => {
2114
+ setIsLinkDropdownOpen(false);
2115
+ setLinkUrl('');
2116
+ setLinkTarget('_blank');
2117
+ setSavedSelection(null);
2118
+ }, className: styles.default, children: "\uCDE8\uC18C" }), _jsx("button", { type: "button", onClick: applyLink, disabled: !linkUrl, className: styles.primary, children: "\uC0BD\uC785" })] })] }))] }), _jsxs("div", { ref: imageButtonRef, style: { position: 'relative' }, children: [_jsx("button", { type: "button", className: styles.toolbarButton, onClick: openImageDropdown, title: "\uC774\uBBF8\uC9C0", children: _jsx("i", { className: styles.image }) }), isImageDropdownOpen && (_jsxs("div", { className: styles.imageDropdown, style: {
2119
+ top: imageButtonRef.current?.getBoundingClientRect().bottom ?? 0,
2120
+ left: imageButtonRef.current?.getBoundingClientRect().left ?? 0
2121
+ }, children: [_jsxs("div", { className: styles.imageTabSection, children: [_jsxs("div", { className: styles.imageTabButtons, children: [_jsx("button", { type: "button", className: imageTabMode === 'file' ? styles.active : '', onClick: () => {
2122
+ setImageTabMode('file');
2123
+ setImageUrl(''); // URL 초기화
2124
+ }, children: "\uD30C\uC77C \uC5C5\uB85C\uB4DC" }), _jsx("button", { type: "button", className: imageTabMode === 'url' ? styles.active : '', onClick: () => {
2125
+ setImageTabMode('url');
2126
+ setImageFile(null); // 파일 초기화
2127
+ setImagePreview(''); // 프리뷰 초기화
2128
+ }, children: "URL \uC785\uB825" })] }), imageTabMode === 'file' && (_jsxs("div", { className: styles.imageFileSection, children: [_jsx("input", { ref: imageFileInputRef, type: "file", accept: "image/*", onChange: handleImageFileSelect, style: { display: 'none' } }), _jsx("button", { type: "button", onClick: () => imageFileInputRef.current?.click(), className: styles.fileSelectButton, children: imageFile ? imageFile.name : '파일 선택' }), imagePreview && (_jsx("div", { className: styles.imagePreviewBox, children: _jsx("img", { src: imagePreview, alt: "Preview" }) }))] })), imageTabMode === 'url' && (_jsx("div", { className: styles.imageUrlSection, children: _jsx("input", { type: "text", value: imageUrl, onChange: (e) => {
2129
+ setImageUrl(e.target.value);
2130
+ }, placeholder: "https://..." }) }))] }), _jsxs("div", { className: styles.imageOptions, children: [_jsxs("div", { className: styles.imageOptionRow, children: [_jsx("label", { children: "\uD06C\uAE30" }), _jsxs("div", { className: styles.imageSizeButtons, children: [_jsx("button", { type: "button", className: imageWidth === '100%' ? styles.active : '', onClick: () => setImageWidth('100%'), children: "100%" }), _jsx("button", { type: "button", className: imageWidth === '75%' ? styles.active : '', onClick: () => setImageWidth('75%'), children: "75%" }), _jsx("button", { type: "button", className: imageWidth === '50%' ? styles.active : '', onClick: () => setImageWidth('50%'), children: "50%" }), _jsx("button", { type: "button", className: imageWidth === 'original' ? styles.active : '', onClick: () => setImageWidth('original'), children: "\uC6D0\uBCF8" })] })] }), _jsxs("div", { className: styles.imageOptionRow, children: [_jsx("label", { children: "\uC815\uB82C" }), _jsxs("div", { className: styles.imageAlignButtons, children: [_jsx("button", { type: "button", className: imageAlign === 'left' ? styles.active : '', onClick: () => setImageAlign('left'), title: "\uC67C\uCABD \uC815\uB82C", children: _jsx("i", { className: styles.alignLeft }) }), _jsx("button", { type: "button", className: imageAlign === 'center' ? styles.active : '', onClick: () => setImageAlign('center'), title: "\uAC00\uC6B4\uB370 \uC815\uB82C", children: _jsx("i", { className: styles.alignCenter }) }), _jsx("button", { type: "button", className: imageAlign === 'right' ? styles.active : '', onClick: () => setImageAlign('right'), title: "\uC624\uB978\uCABD \uC815\uB82C", children: _jsx("i", { className: styles.alignRight }) })] })] }), _jsxs("div", { className: styles.imageOptionRow, children: [_jsx("label", { children: "\uB300\uCCB4 \uD14D\uC2A4\uD2B8" }), _jsx("input", { type: "text", value: imageAlt, onChange: (e) => setImageAlt(e.target.value), placeholder: "\uC774\uBBF8\uC9C0 \uC124\uBA85..." })] })] }), _jsxs("div", { className: styles.imageActions, children: [_jsx("button", { type: "button", onClick: () => {
2131
+ setIsImageDropdownOpen(false);
2132
+ setImageTabMode('file'); // 탭 모드 초기화
2133
+ setImageUrl('');
2134
+ setImageFile(null);
2135
+ setImagePreview('');
2136
+ setImageWidth('original'); // 원본으로 초기화
2137
+ setImageAlign('left'); // 좌측으로 초기화
2138
+ setImageAlt('');
2139
+ setSavedImageSelection(null); // 저장된 선택 영역 초기화
2140
+ }, className: styles.default, children: "\uCDE8\uC18C" }), _jsx("button", { type: "button", onClick: insertImage, disabled: !imageUrl && !imageFile, className: styles.primary, children: "\uC0BD\uC785" })] })] }))] }), _jsxs("div", { ref: youtubeButtonRef, style: { position: 'relative' }, children: [_jsx("button", { type: "button", className: styles.toolbarButton, onClick: (e) => {
2141
+ e.stopPropagation();
2142
+ // 현재 선택 영역 저장
2143
+ const selection = window.getSelection();
2144
+ if (selection && selection.rangeCount > 0) {
2145
+ const range = selection.getRangeAt(0).cloneRange();
2146
+ setSavedYoutubeSelection(range);
2147
+ }
2148
+ else {
2149
+ setSavedYoutubeSelection(null);
2150
+ }
2151
+ setIsYoutubeDropdownOpen(true);
2152
+ setIsImageDropdownOpen(false);
2153
+ setIsParagraphDropdownOpen(false);
2154
+ setIsTextColorOpen(false);
2155
+ setIsBgColorOpen(false);
2156
+ setIsAlignDropdownOpen(false);
2157
+ setIsLinkDropdownOpen(false);
2158
+ }, title: "\uC720\uD29C\uBE0C", children: _jsx("i", { className: styles.youtube }) }), isYoutubeDropdownOpen && (_jsxs("div", { className: styles.imageDropdown, style: {
2159
+ top: youtubeButtonRef.current?.getBoundingClientRect().bottom ?? 0,
2160
+ left: youtubeButtonRef.current?.getBoundingClientRect().left ?? 0
2161
+ }, children: [_jsxs("div", { className: styles.imageTabSection, children: [_jsx("div", { className: styles.imageTabButtons, children: _jsx("button", { type: "button", className: styles.active, style: { width: '100%' }, children: "\uC720\uD29C\uBE0C URL" }) }), _jsx("div", { className: styles.imageUrlSection, children: _jsx("input", { type: "text", value: youtubeUrl, onChange: (e) => setYoutubeUrl(e.target.value), placeholder: "https://www.youtube.com/watch?v=... \uB610\uB294 https://youtu.be/...", autoFocus: true }) })] }), _jsxs("div", { className: styles.imageOptions, children: [_jsxs("div", { className: styles.imageOptionRow, children: [_jsx("label", { children: "\uD06C\uAE30" }), _jsxs("div", { className: styles.imageSizeButtons, children: [_jsx("button", { type: "button", className: youtubeWidth === '100%' ? styles.active : '', onClick: () => setYoutubeWidth('100%'), children: "100%" }), _jsx("button", { type: "button", className: youtubeWidth === '75%' ? styles.active : '', onClick: () => setYoutubeWidth('75%'), children: "75%" }), _jsx("button", { type: "button", className: youtubeWidth === '50%' ? styles.active : '', onClick: () => setYoutubeWidth('50%'), children: "50%" }), _jsx("button", { type: "button", className: youtubeWidth === 'original' ? styles.active : '', onClick: () => setYoutubeWidth('original'), children: "\uC6D0\uBCF8" })] })] }), _jsxs("div", { className: styles.imageOptionRow, children: [_jsx("label", { children: "\uC815\uB82C" }), _jsxs("div", { className: styles.imageAlignButtons, children: [_jsx("button", { type: "button", className: youtubeAlign === 'left' ? styles.active : '', onClick: () => setYoutubeAlign('left'), title: "\uC67C\uCABD \uC815\uB82C", children: _jsx("i", { className: styles.alignLeft }) }), _jsx("button", { type: "button", className: youtubeAlign === 'center' ? styles.active : '', onClick: () => setYoutubeAlign('center'), title: "\uAC00\uC6B4\uB370 \uC815\uB82C", children: _jsx("i", { className: styles.alignCenter }) }), _jsx("button", { type: "button", className: youtubeAlign === 'right' ? styles.active : '', onClick: () => setYoutubeAlign('right'), title: "\uC624\uB978\uCABD \uC815\uB82C", children: _jsx("i", { className: styles.alignRight }) })] })] })] }), _jsxs("div", { className: styles.imageActions, children: [_jsx("button", { type: "button", className: styles.default, onClick: () => {
2162
+ setIsYoutubeDropdownOpen(false);
2163
+ setYoutubeUrl('');
2164
+ setYoutubeWidth('100%');
2165
+ setYoutubeAlign('center');
2166
+ setSavedYoutubeSelection(null);
2167
+ }, children: "\uCDE8\uC18C" }), _jsx("button", { type: "button", className: styles.primary, onClick: () => insertYoutube(), disabled: !youtubeUrl, children: "\uC0BD\uC785" })] })] }))] })] }), _jsx("div", { className: styles.toolbarGroup, children: _jsx("button", { type: "button", className: styles.toolbarButton, onClick: () => execCommand('removeFormat'), title: "\uC11C\uC2DD \uC9C0\uC6B0\uAE30", children: _jsx("i", { className: styles.eraser }) }) }), _jsx("div", { className: styles.toolbarGroup, children: _jsx("button", { type: "button", className: `${styles.toolbarButton} ${isCodeView ? styles.active : ''}`, onClick: toggleCodeView, title: isCodeView ? "에디터로 전환" : "HTML 코드보기", children: _jsx("i", { className: styles.code }) }) })] }), _jsx("div", { ref: containerRef, className: `${styles.editorContainer} ${resizable ? styles.resizable : ''}`, style: {
2168
+ height: height === 'contents' ? 'auto' : (height || '300px'),
2169
+ minHeight: minHeight || (height === 'contents' ? '100px' : '200px'),
2170
+ maxHeight: maxHeight || (height === 'contents' ? undefined : undefined),
2171
+ display: 'flex',
2172
+ flexDirection: 'column',
2173
+ resize: resizable ? 'vertical' : 'none',
2174
+ overflow: 'auto'
2175
+ }, children: isCodeView ? (_jsx("textarea", { ref: codeEditorRef, className: styles.codeEditor, value: codeContent, onChange: handleCodeChange, spellCheck: false, style: {
2176
+ flex: height === 'contents' ? '0 0 auto' : 1,
2177
+ minHeight: height === 'contents' ? 'auto' : 0,
2178
+ height: height === 'contents' && savedEditorHeight ? `${savedEditorHeight}px` : undefined,
2179
+ resize: 'none'
2180
+ }, placeholder: placeholder })) : (_jsx("div", { ref: editorRef, id: editorID, className: styles.editorContent, contentEditable: true, onInput: handleInput, onPaste: handlePaste, onClick: handleEditorClick, onKeyUp: () => {
2181
+ detectCurrentParagraphStyle();
2182
+ detectCurrentAlign();
2183
+ }, onKeyDown: handleKeyDown, style: {
2184
+ flex: height === 'contents' ? '0 0 auto' : 1,
2185
+ minHeight: height === 'contents' ? 'auto' : 0,
2186
+ overflowY: height === 'contents' ? 'visible' : 'auto'
2187
+ }, "data-placeholder": placeholder })) }), validator && message && (_jsx("div", { className: styles.validator, children: message })), _jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", onChange: handleFileUpload, style: { display: 'none' } }), isEditLinkPopupOpen && selectedLinkElement && (_jsx("div", { className: styles.editLinkPopup, style: {
2188
+ position: 'absolute',
2189
+ top: selectedLinkElement.offsetTop + selectedLinkElement.offsetHeight + 5,
2190
+ left: selectedLinkElement.offsetLeft
2191
+ }, children: _jsxs("div", { className: styles.editLinkContent, children: [_jsxs("div", { className: styles.editLinkInput, children: [_jsx("label", { children: "URL \uC218\uC815" }), _jsx("input", { type: "text", value: editLinkUrl, onChange: (e) => setEditLinkUrl(e.target.value), placeholder: "https://...", autoFocus: true })] }), _jsxs("div", { className: styles.editLinkTarget, children: [_jsxs("label", { children: [_jsx("input", { type: "radio", value: "_blank", checked: editLinkTarget === '_blank', onChange: (e) => setEditLinkTarget(e.target.value) }), "\uC0C8 \uCC3D\uC5D0\uC11C \uC5F4\uAE30"] }), _jsxs("label", { children: [_jsx("input", { type: "radio", value: "_self", checked: editLinkTarget === '_self', onChange: (e) => setEditLinkTarget(e.target.value) }), "\uD604\uC7AC \uCC3D\uC5D0\uC11C \uC5F4\uAE30"] })] }), _jsxs("div", { className: styles.editLinkActions, children: [_jsx("button", { type: "button", onClick: removeLink, className: styles.danger, children: "\uB9C1\uD06C \uC0AD\uC81C" }), _jsxs("div", { style: { display: 'flex', gap: '8px' }, children: [_jsx("button", { type: "button", onClick: () => {
2192
+ setIsEditLinkPopupOpen(false);
2193
+ setSelectedLinkElement(null);
2194
+ setEditLinkUrl('');
2195
+ setEditLinkTarget('_self');
2196
+ }, className: styles.default, children: "\uCDE8\uC18C" }), _jsx("button", { type: "button", onClick: updateLink, disabled: !editLinkUrl, className: styles.primary, children: "\uC801\uC6A9" })] })] })] }) })), isImageEditPopupOpen && selectedImage && (() => {
2197
+ // 이미지의 wrapper를 찾기 (wrapper가 있으면 wrapper 기준, 없으면 이미지 기준)
2198
+ const imageWrapper = selectedImage.parentElement?.classList.contains('image-wrapper')
2199
+ ? selectedImage.parentElement
2200
+ : selectedImage;
2201
+ return (_jsxs("div", { className: styles.imageDropdown, style: {
2202
+ position: 'fixed',
2203
+ top: imageWrapper.getBoundingClientRect().bottom + 10,
2204
+ left: Math.max(10, Math.min(imageWrapper.getBoundingClientRect().left + (imageWrapper.getBoundingClientRect().width / 2) - 180, window.innerWidth - 370)),
2205
+ zIndex: 9999,
2206
+ minWidth: '360px',
2207
+ maxWidth: '90%'
2208
+ }, children: [_jsx("h3", { style: { margin: '0 0 15px 0', fontSize: '16px', fontWeight: '600' }, children: "\uC774\uBBF8\uC9C0 \uD3B8\uC9D1" }), _jsxs("div", { className: styles.imageOptions, style: { marginBottom: '0' }, children: [_jsxs("div", { className: styles.imageOptionRow, children: [_jsx("label", { children: "\uD06C\uAE30" }), _jsxs("div", { className: styles.imageSizeButtons, children: [_jsx("button", { type: "button", onClick: () => setEditImageWidth('100%'), className: editImageWidth === '100%' ? styles.active : '', children: "100%" }), _jsx("button", { type: "button", onClick: () => setEditImageWidth('75%'), className: editImageWidth === '75%' ? styles.active : '', children: "75%" }), _jsx("button", { type: "button", onClick: () => setEditImageWidth('50%'), className: editImageWidth === '50%' ? styles.active : '', children: "50%" }), _jsx("button", { type: "button", onClick: () => setEditImageWidth('original'), className: editImageWidth === 'original' ? styles.active : '', children: "\uC6D0\uBCF8" })] })] }), _jsxs("div", { className: styles.imageOptionRow, children: [_jsx("label", { children: "\uC815\uB82C" }), _jsxs("div", { className: styles.imageAlignButtons, children: [_jsx("button", { type: "button", onClick: () => setEditImageAlign('left'), title: "\uC67C\uCABD \uC815\uB82C", className: editImageAlign === 'left' ? styles.active : '', children: _jsx("i", { className: styles.alignLeft }) }), _jsx("button", { type: "button", onClick: () => setEditImageAlign('center'), title: "\uAC00\uC6B4\uB370 \uC815\uB82C", className: editImageAlign === 'center' ? styles.active : '', children: _jsx("i", { className: styles.alignCenter }) }), _jsx("button", { type: "button", onClick: () => setEditImageAlign('right'), title: "\uC624\uB978\uCABD \uC815\uB82C", className: editImageAlign === 'right' ? styles.active : '', children: _jsx("i", { className: styles.alignRight }) })] })] }), _jsxs("div", { className: styles.imageOptionRow, children: [_jsx("label", { children: "\uB300\uCCB4 \uD14D\uC2A4\uD2B8" }), _jsx("input", { type: "text", value: editImageAlt, onChange: (e) => setEditImageAlt(e.target.value), placeholder: "\uC774\uBBF8\uC9C0 \uC124\uBA85..." })] })] }), _jsxs("div", { className: styles.imageActions, children: [_jsx("button", { type: "button", onClick: deleteImage, className: styles.danger, children: "\uC0AD\uC81C" }), _jsxs("div", { style: { display: 'flex', gap: '8px' }, children: [_jsx("button", { type: "button", onClick: deselectImage, className: styles.default, children: "\uCDE8\uC18C" }), _jsx("button", { type: "button", onClick: applyImageEdit, className: styles.primary, children: "\uC801\uC6A9" })] })] })] }));
2209
+ })(), isYoutubeEditPopupOpen && selectedYoutube && (() => {
2210
+ // 유튜브의 wrapper를 찾기
2211
+ const youtubeWrapper = selectedYoutube.parentElement?.classList.contains('youtube-wrapper')
2212
+ ? selectedYoutube.parentElement
2213
+ : selectedYoutube;
2214
+ return (_jsxs("div", { className: styles.imageDropdown, style: {
2215
+ position: 'fixed',
2216
+ top: youtubeWrapper.getBoundingClientRect().bottom + 10,
2217
+ left: Math.max(10, Math.min(youtubeWrapper.getBoundingClientRect().left + (youtubeWrapper.getBoundingClientRect().width / 2) - 180, window.innerWidth - 370)),
2218
+ zIndex: 9999,
2219
+ minWidth: '360px',
2220
+ maxWidth: '90%'
2221
+ }, children: [_jsx("h3", { style: { margin: '0 0 15px 0', fontSize: '16px', fontWeight: '600' }, children: "\uC720\uD29C\uBE0C \uD3B8\uC9D1" }), _jsxs("div", { className: styles.imageOptions, style: { marginBottom: '0' }, children: [_jsxs("div", { className: styles.imageOptionRow, children: [_jsx("label", { children: "\uD06C\uAE30" }), _jsxs("div", { className: styles.imageSizeButtons, children: [_jsx("button", { type: "button", onClick: () => setEditYoutubeWidth('100%'), className: editYoutubeWidth === '100%' ? styles.active : '', children: "100%" }), _jsx("button", { type: "button", onClick: () => setEditYoutubeWidth('75%'), className: editYoutubeWidth === '75%' ? styles.active : '', children: "75%" }), _jsx("button", { type: "button", onClick: () => setEditYoutubeWidth('50%'), className: editYoutubeWidth === '50%' ? styles.active : '', children: "50%" }), _jsx("button", { type: "button", onClick: () => setEditYoutubeWidth('original'), className: editYoutubeWidth === 'original' ? styles.active : '', children: "\uC6D0\uBCF8" })] })] }), _jsxs("div", { className: styles.imageOptionRow, children: [_jsx("label", { children: "\uC815\uB82C" }), _jsxs("div", { className: styles.imageAlignButtons, children: [_jsx("button", { type: "button", onClick: () => setEditYoutubeAlign('left'), title: "\uC67C\uCABD \uC815\uB82C", className: editYoutubeAlign === 'left' ? styles.active : '', children: _jsx("i", { className: styles.alignLeft }) }), _jsx("button", { type: "button", onClick: () => setEditYoutubeAlign('center'), title: "\uAC00\uC6B4\uB370 \uC815\uB82C", className: editYoutubeAlign === 'center' ? styles.active : '', children: _jsx("i", { className: styles.alignCenter }) }), _jsx("button", { type: "button", onClick: () => setEditYoutubeAlign('right'), title: "\uC624\uB978\uCABD \uC815\uB82C", className: editYoutubeAlign === 'right' ? styles.active : '', children: _jsx("i", { className: styles.alignRight }) })] })] })] }), _jsxs("div", { className: styles.imageActions, children: [_jsx("button", { type: "button", className: styles.danger, onClick: deleteYoutube, children: "\uC0AD\uC81C" }), _jsxs("div", { style: { display: 'flex', gap: '8px' }, children: [_jsx("button", { type: "button", className: styles.default, onClick: deselectYoutube, children: "\uCDE8\uC18C" }), _jsx("button", { type: "button", className: styles.primary, onClick: applyYoutubeEdit, children: "\uC801\uC6A9" })] })] })] }));
2222
+ })()] }));
2223
+ };
2224
+ export default Editor;