podo-ui 0.3.13 → 0.3.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/react/atom/editor.d.ts +3 -1
- package/dist/react/atom/editor.d.ts.map +1 -1
- package/dist/react/atom/editor.js +678 -32
- package/dist/react/atom/editor.module.scss +135 -0
- package/dist/react/atom/input.module.scss +1 -1
- package/dist/react/atom/textarea.module.scss +1 -1
- package/dist/react/molecule/field.module.scss +1 -1
- package/dist/react/molecule/pagination.module.scss +1 -1
- package/dist/react/molecule/toast-container.module.scss +1 -1
- package/package.json +4 -5
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import Field from './react/molecule/field';
|
|
|
7
7
|
declare const Form: {
|
|
8
8
|
Input: import("react").FC<import("./react/atom/input").InputWrapperProps>;
|
|
9
9
|
Textarea: import("react").FC<import("./react/atom/textarea").TextareaWrapperProps>;
|
|
10
|
-
Editor: ({ value, width, height, minHeight, maxHeight, resizable, onChange, validator, placeholder, }: import("./react/atom/editor").EditorProps) => import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
Editor: ({ value, width, height, minHeight, maxHeight, resizable, onChange, validator, placeholder, toolbar, }: import("./react/atom/editor").EditorProps) => import("react/jsx-runtime").JSX.Element;
|
|
11
11
|
EditorView: ({ value, className }: import("./react/atom/editor-view").EditorViewProps) => import("react/jsx-runtime").JSX.Element;
|
|
12
12
|
Field: ({ label, labelClass, required, helper, helperClass, children, validator, value, setClassName, className, }: import("./react/molecule/field").FieldProps) => import("react/jsx-runtime").JSX.Element;
|
|
13
13
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
export type ToolbarItem = 'undo-redo' | 'paragraph' | 'text-style' | 'color' | 'align' | 'list' | 'table' | 'link' | 'image' | 'youtube' | 'format' | 'code';
|
|
2
3
|
export interface EditorProps {
|
|
3
4
|
value: string;
|
|
4
5
|
width?: string;
|
|
@@ -9,7 +10,8 @@ export interface EditorProps {
|
|
|
9
10
|
onChange: (content: string) => void;
|
|
10
11
|
validator?: z.ZodType<unknown>;
|
|
11
12
|
placeholder?: string;
|
|
13
|
+
toolbar?: ToolbarItem[];
|
|
12
14
|
}
|
|
13
|
-
declare const Editor: ({ value, width, height, minHeight, maxHeight, resizable, onChange, validator, placeholder, }: EditorProps) => import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
declare const Editor: ({ value, width, height, minHeight, maxHeight, resizable, onChange, validator, placeholder, toolbar, }: EditorProps) => import("react/jsx-runtime").JSX.Element;
|
|
14
16
|
export default Editor;
|
|
15
17
|
//# sourceMappingURL=editor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../../../react/atom/editor.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,SAAS,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../../../react/atom/editor.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,MAAM,MAAM,WAAW,GACnB,WAAW,GACX,WAAW,GACX,YAAY,GACZ,OAAO,GACP,OAAO,GACP,MAAM,GACN,OAAO,GACP,MAAM,GACN,OAAO,GACP,SAAS,GACT,QAAQ,GACR,MAAM,CAAC;AAEX,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,SAAS,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,QAAA,MAAM,MAAM,0GAWT,WAAW,4CAmrIb,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useRef, useEffect, useState, useCallback } from 'react';
|
|
3
3
|
import { v4 as uuid } from 'uuid';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import styles from './editor.module.scss';
|
|
6
|
-
const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHeight, resizable = false, onChange, validator, placeholder = '내용을 입력하세요...', }) => {
|
|
6
|
+
const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHeight, resizable = false, onChange, validator, placeholder = '내용을 입력하세요...', toolbar, }) => {
|
|
7
7
|
const [message, setMessage] = useState('');
|
|
8
8
|
const [statusClass, setStatusClass] = useState('');
|
|
9
9
|
const [currentParagraphStyle, setCurrentParagraphStyle] = useState('p');
|
|
@@ -70,22 +70,59 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
70
70
|
const imageButtonRef = useRef(null);
|
|
71
71
|
const youtubeButtonRef = useRef(null);
|
|
72
72
|
const imageFileInputRef = useRef(null);
|
|
73
|
+
const tableButtonRef = useRef(null);
|
|
73
74
|
// 클라이언트에서만 ID 생성 (Vite React용)
|
|
74
75
|
const [editorID, setEditorID] = useState('podo-editor');
|
|
76
|
+
// 표 삽입 관련 상태
|
|
77
|
+
const [isTableDropdownOpen, setIsTableDropdownOpen] = useState(false);
|
|
78
|
+
const [tableRows, setTableRows] = useState(0);
|
|
79
|
+
const [tableCols, setTableCols] = useState(0);
|
|
80
|
+
const [savedTableSelection, setSavedTableSelection] = useState(null);
|
|
81
|
+
// 표 컨텍스트 메뉴 관련 상태
|
|
82
|
+
const [isTableContextMenuOpen, setIsTableContextMenuOpen] = useState(false);
|
|
83
|
+
const [tableContextMenuPosition, setTableContextMenuPosition] = useState({ x: 0, y: 0 });
|
|
84
|
+
const [selectedTableCell, setSelectedTableCell] = useState(null);
|
|
85
|
+
const [isTableCellColorOpen, setIsTableCellColorOpen] = useState(false);
|
|
86
|
+
const tableContextMenuRef = useRef(null);
|
|
87
|
+
// 다중 셀 선택 관련 상태
|
|
88
|
+
const [selectedTableCells, setSelectedTableCells] = useState([]);
|
|
89
|
+
const [isSelectingCells, setIsSelectingCells] = useState(false);
|
|
90
|
+
const [selectionStartCell, setSelectionStartCell] = useState(null);
|
|
91
|
+
const isSelectingCellsRef = useRef(false); // 최신 상태 추적을 위한 ref
|
|
92
|
+
const justFinishedDraggingRef = useRef(false); // 드래그가 방금 끝났는지 추적
|
|
93
|
+
const isMouseDownRef = useRef(false); // 마우스 버튼이 눌려있는지 추적
|
|
94
|
+
// 툴바 설정 (기본값: 모든 툴)
|
|
95
|
+
const defaultToolbar = [
|
|
96
|
+
'undo-redo',
|
|
97
|
+
'paragraph',
|
|
98
|
+
'text-style',
|
|
99
|
+
'color',
|
|
100
|
+
'align',
|
|
101
|
+
'list',
|
|
102
|
+
'table',
|
|
103
|
+
'link',
|
|
104
|
+
'image',
|
|
105
|
+
'youtube',
|
|
106
|
+
'format',
|
|
107
|
+
'code',
|
|
108
|
+
];
|
|
109
|
+
const activeToolbar = toolbar || defaultToolbar;
|
|
110
|
+
// 특정 툴바 아이템이 활성화되어 있는지 확인
|
|
111
|
+
const isToolbarItemEnabled = (item) => activeToolbar.includes(item);
|
|
75
112
|
// 색상 팔레트 정의 (이미지 기반)
|
|
76
113
|
const colorPalette = [
|
|
77
|
-
// 첫 번째 줄: 순수 색상
|
|
78
|
-
['#ff0000', '#ff8000', '#ffff00', '#80ff00', '#00ffff', '#0080ff', '#0000ff', '#8000ff', '#ff00ff', '#000000'],
|
|
114
|
+
// 첫 번째 줄: 순수 색상 + 흰색/검은색
|
|
115
|
+
['#ff0000', '#ff8000', '#ffff00', '#80ff00', '#00ffff', '#0080ff', '#0000ff', '#8000ff', '#ff00ff', '#ffffff', '#000000'],
|
|
79
116
|
// 두 번째 줄: 매우 밝은 톤 (90% 밝기)
|
|
80
|
-
['#ffcccc', '#ffe0cc', '#ffffcc', '#e0ffcc', '#ccffff', '#cce0ff', '#ccccff', '#e0ccff', '#ffccff', '#cccccc'],
|
|
117
|
+
['#ffcccc', '#ffe0cc', '#ffffcc', '#e0ffcc', '#ccffff', '#cce0ff', '#ccccff', '#e0ccff', '#ffccff', '#f5f5f5', '#cccccc'],
|
|
81
118
|
// 세 번째 줄: 밝은 톤 (70% 밝기)
|
|
82
|
-
['#ff9999', '#ffcc99', '#ffff99', '#ccff99', '#99ffff', '#99ccff', '#9999ff', '#cc99ff', '#ff99ff', '#999999'],
|
|
119
|
+
['#ff9999', '#ffcc99', '#ffff99', '#ccff99', '#99ffff', '#99ccff', '#9999ff', '#cc99ff', '#ff99ff', '#e6e6e6', '#999999'],
|
|
83
120
|
// 네 번째 줄: 중간 톤 (50% 밝기)
|
|
84
|
-
['#ff6666', '#ffb366', '#ffff66', '#b3ff66', '#66ffff', '#66b3ff', '#6666ff', '#b366ff', '#ff66ff', '#666666'],
|
|
121
|
+
['#ff6666', '#ffb366', '#ffff66', '#b3ff66', '#66ffff', '#66b3ff', '#6666ff', '#b366ff', '#ff66ff', '#d9d9d9', '#666666'],
|
|
85
122
|
// 다섯 번째 줄: 어두운 톤 (30% 밝기)
|
|
86
|
-
['#cc0000', '#cc6600', '#cccc00', '#66cc00', '#00cccc', '#0066cc', '#0000cc', '#6600cc', '#cc00cc', '#333333'],
|
|
123
|
+
['#cc0000', '#cc6600', '#cccc00', '#66cc00', '#00cccc', '#0066cc', '#0000cc', '#6600cc', '#cc00cc', '#b3b3b3', '#333333'],
|
|
87
124
|
// 여섯 번째 줄: 매우 어두운 톤 (15% 밝기)
|
|
88
|
-
['#800000', '#804000', '#808000', '#408000', '#008080', '#004080', '#000080', '#400080', '#800080', '#1a1a1a'],
|
|
125
|
+
['#800000', '#804000', '#808000', '#408000', '#008080', '#004080', '#000080', '#400080', '#800080', '#808080', '#1a1a1a'],
|
|
89
126
|
];
|
|
90
127
|
// 정렬 옵션 정의
|
|
91
128
|
const alignOptions = [
|
|
@@ -346,7 +383,8 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
346
383
|
// 지원하는 태그와 스타일 정의
|
|
347
384
|
const allowedTags = ['P', 'BR', 'STRONG', 'B', 'EM', 'I', 'U', 'S', 'STRIKE', 'DEL',
|
|
348
385
|
'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'PRE',
|
|
349
|
-
'UL', 'OL', 'LI', 'A', 'IMG', 'SPAN', 'DIV'
|
|
386
|
+
'UL', 'OL', 'LI', 'A', 'IMG', 'SPAN', 'DIV',
|
|
387
|
+
'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD'];
|
|
350
388
|
const allowedStyles = ['color', 'background-color', 'text-align'];
|
|
351
389
|
// 모든 요소를 순회하면서 정리
|
|
352
390
|
const cleanElement = (element) => {
|
|
@@ -383,14 +421,45 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
383
421
|
newElement.setAttribute('alt', element.getAttribute('alt'));
|
|
384
422
|
}
|
|
385
423
|
}
|
|
424
|
+
// Table 요소에 에디터 기본 스타일 적용 (insertTable과 동일)
|
|
425
|
+
if (tagName === 'TABLE') {
|
|
426
|
+
newElement.style.borderCollapse = 'collapse';
|
|
427
|
+
newElement.style.width = '100%';
|
|
428
|
+
newElement.style.margin = '10px 0';
|
|
429
|
+
newElement.setAttribute('border', '1');
|
|
430
|
+
newElement.style.border = '1px solid #ddd';
|
|
431
|
+
}
|
|
432
|
+
if (tagName === 'TH' || tagName === 'TD') {
|
|
433
|
+
newElement.style.border = '1px solid #ddd';
|
|
434
|
+
newElement.style.padding = '8px';
|
|
435
|
+
if (tagName === 'TD') {
|
|
436
|
+
newElement.style.minWidth = '50px';
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (tagName === 'TH') {
|
|
440
|
+
newElement.style.fontWeight = 'bold';
|
|
441
|
+
}
|
|
386
442
|
// 스타일 복원 (허용된 것만)
|
|
387
443
|
if (element instanceof HTMLElement && element.style) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
444
|
+
// 테이블 셀의 경우 배경색과 정렬만 허용
|
|
445
|
+
if (tagName === 'TD' || tagName === 'TH') {
|
|
446
|
+
const cellAllowedStyles = ['background-color', 'text-align'];
|
|
447
|
+
cellAllowedStyles.forEach(styleName => {
|
|
448
|
+
const value = element.style.getPropertyValue(styleName);
|
|
449
|
+
if (value) {
|
|
450
|
+
newElement.style.setProperty(styleName, value);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
// 일반 요소는 허용된 스타일만
|
|
456
|
+
allowedStyles.forEach(styleName => {
|
|
457
|
+
const value = element.style.getPropertyValue(styleName);
|
|
458
|
+
if (value) {
|
|
459
|
+
newElement.style.setProperty(styleName, value);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
394
463
|
}
|
|
395
464
|
// 자식 요소 처리
|
|
396
465
|
Array.from(element.childNodes).forEach(child => {
|
|
@@ -484,6 +553,10 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
484
553
|
}
|
|
485
554
|
else {
|
|
486
555
|
// 일반 모드에서 코드보기로 전환
|
|
556
|
+
// 셀 선택 상태 해제
|
|
557
|
+
if (selectedTableCells.length > 0) {
|
|
558
|
+
clearCellSelection();
|
|
559
|
+
}
|
|
487
560
|
if (editorRef.current) {
|
|
488
561
|
// height가 contents일 때 현재 에디터 높이 저장
|
|
489
562
|
if (height === 'contents') {
|
|
@@ -914,6 +987,28 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
914
987
|
if (selectedYoutube && !target.closest('.youtube-wrapper') && !isResizing) {
|
|
915
988
|
deselectYoutube();
|
|
916
989
|
}
|
|
990
|
+
// 표 컨텍스트 메뉴 닫기
|
|
991
|
+
if (isTableContextMenuOpen && !target.closest(`.${styles.tableContextMenu}`)) {
|
|
992
|
+
setIsTableContextMenuOpen(false);
|
|
993
|
+
setSelectedTableCell(null);
|
|
994
|
+
setIsTableCellColorOpen(false);
|
|
995
|
+
}
|
|
996
|
+
// 표 셀 클릭 시에는 선택 유지
|
|
997
|
+
const clickedCell = target.closest('td');
|
|
998
|
+
console.log('⚪ handleEditorClick - 에디터 클릭');
|
|
999
|
+
console.log(' - clickedCell:', !!clickedCell);
|
|
1000
|
+
console.log(' - selectedTableCells.length:', selectedTableCells.length);
|
|
1001
|
+
console.log(' - justFinishedDraggingRef.current:', justFinishedDraggingRef.current);
|
|
1002
|
+
// 드래그가 방금 끝난 경우 선택 해제하지 않음
|
|
1003
|
+
if (justFinishedDraggingRef.current) {
|
|
1004
|
+
console.log(' - 드래그 직후이므로 선택 유지');
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
// 표 셀 외부를 클릭한 경우에만 선택 해제
|
|
1008
|
+
if (!clickedCell && selectedTableCells.length > 0) {
|
|
1009
|
+
console.log(' - 표 외부 클릭, 선택 해제 호출');
|
|
1010
|
+
clearCellSelection();
|
|
1011
|
+
}
|
|
917
1012
|
// 링크 요소인지 확인
|
|
918
1013
|
const linkElement = target.closest('a');
|
|
919
1014
|
if (linkElement && editorRef.current?.contains(linkElement)) {
|
|
@@ -929,6 +1024,136 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
929
1024
|
detectCurrentAlign();
|
|
930
1025
|
}
|
|
931
1026
|
};
|
|
1027
|
+
// 표 셀 마우스 다운 (드래그 선택 시작)
|
|
1028
|
+
const handleCellMouseDown = useCallback((e) => {
|
|
1029
|
+
const target = e.target;
|
|
1030
|
+
const cell = target.closest('td');
|
|
1031
|
+
if (cell && editorRef.current?.contains(cell)) {
|
|
1032
|
+
// 이미지나 이미지 컨테이너를 드래그하는 경우 셀 선택 방지
|
|
1033
|
+
if (target.tagName === 'IMG' || target.classList.contains('image-container')) {
|
|
1034
|
+
console.log('🔵 handleCellMouseDown - 이미지 드래그 감지, 셀 선택 무시');
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
console.log('🔵 handleCellMouseDown - 셀 클릭');
|
|
1038
|
+
// 마우스 다운 상태 설정
|
|
1039
|
+
isMouseDownRef.current = true;
|
|
1040
|
+
console.log(' - isMouseDownRef.current = true');
|
|
1041
|
+
// 드래그 시작 셀 설정
|
|
1042
|
+
setSelectionStartCell(cell);
|
|
1043
|
+
// 이미 선택된 셀을 클릭한 경우 선택 유지
|
|
1044
|
+
const isAlreadySelected = cell.classList.contains('selected-cell');
|
|
1045
|
+
console.log(' - isAlreadySelected:', isAlreadySelected);
|
|
1046
|
+
console.log(' - shift 키:', e.shiftKey);
|
|
1047
|
+
// 새로운 셀을 클릭하거나 Shift 키를 누르지 않은 경우에만 기존 선택 해제
|
|
1048
|
+
if (!isAlreadySelected && !e.shiftKey) {
|
|
1049
|
+
const allCells = editorRef.current.querySelectorAll('.selected-cell');
|
|
1050
|
+
console.log(' - 기존 선택 해제, 선택된 셀 수:', allCells.length);
|
|
1051
|
+
allCells.forEach(c => c.classList.remove('selected-cell'));
|
|
1052
|
+
setSelectedTableCells([]);
|
|
1053
|
+
}
|
|
1054
|
+
else {
|
|
1055
|
+
console.log(' - 기존 선택 유지');
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}, []);
|
|
1059
|
+
// 표 셀 마우스 이동 (드래그 선택 중)
|
|
1060
|
+
const handleCellMouseMove = useCallback((e) => {
|
|
1061
|
+
const target = e.target;
|
|
1062
|
+
const cell = target.closest('td');
|
|
1063
|
+
if (!cell || !editorRef.current?.contains(cell))
|
|
1064
|
+
return;
|
|
1065
|
+
// 마우스가 눌려있지 않으면 드래그 불가
|
|
1066
|
+
if (!isMouseDownRef.current) {
|
|
1067
|
+
console.log('🟢 handleCellMouseMove - 마우스가 눌려있지 않음, 무시');
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
// selectionStartCell이 있고, 다른 셀로 이동한 경우에만 드래그 선택 모드 활성화
|
|
1071
|
+
if (selectionStartCell && cell !== selectionStartCell && !isSelectingCellsRef.current) {
|
|
1072
|
+
console.log('🟢 handleCellMouseMove - 드래그 선택 모드 활성화');
|
|
1073
|
+
isSelectingCellsRef.current = true;
|
|
1074
|
+
setIsSelectingCells(true);
|
|
1075
|
+
e.preventDefault();
|
|
1076
|
+
e.stopPropagation();
|
|
1077
|
+
}
|
|
1078
|
+
if (!isSelectingCellsRef.current || !selectionStartCell)
|
|
1079
|
+
return;
|
|
1080
|
+
e.preventDefault();
|
|
1081
|
+
e.stopPropagation();
|
|
1082
|
+
// 범위 내 모든 셀 선택
|
|
1083
|
+
const cellsInRange = getCellsInRange(selectionStartCell, cell);
|
|
1084
|
+
console.log('🟢 handleCellMouseMove - 범위 선택, 셀 수:', cellsInRange.length);
|
|
1085
|
+
// 기존 선택 클래스 제거
|
|
1086
|
+
const allSelectedCells = editorRef.current.querySelectorAll('.selected-cell');
|
|
1087
|
+
allSelectedCells.forEach(c => c.classList.remove('selected-cell'));
|
|
1088
|
+
// 새 선택 적용
|
|
1089
|
+
setSelectedTableCells(cellsInRange);
|
|
1090
|
+
cellsInRange.forEach(c => c.classList.add('selected-cell'));
|
|
1091
|
+
}, [selectionStartCell]);
|
|
1092
|
+
// 표 셀 마우스 업 (드래그 선택 종료)
|
|
1093
|
+
const handleCellMouseUp = useCallback((e) => {
|
|
1094
|
+
const target = e.target;
|
|
1095
|
+
const cell = target.closest('td');
|
|
1096
|
+
console.log('🟡 handleCellMouseUp - 마우스 업');
|
|
1097
|
+
console.log(' - isSelectingCellsRef.current:', isSelectingCellsRef.current);
|
|
1098
|
+
console.log(' - isMouseDownRef.current:', isMouseDownRef.current);
|
|
1099
|
+
console.log(' - 현재 선택된 셀 수:', editorRef.current?.querySelectorAll('.selected-cell').length);
|
|
1100
|
+
// 드래그 선택 중이었다면 플래그 설정
|
|
1101
|
+
if (isSelectingCellsRef.current) {
|
|
1102
|
+
console.log(' - 드래그 중이었음, 플래그 설정');
|
|
1103
|
+
// 셀 내부에서 마우스 업한 경우 이벤트 방지
|
|
1104
|
+
if (cell && editorRef.current?.contains(cell)) {
|
|
1105
|
+
e.preventDefault();
|
|
1106
|
+
e.stopPropagation();
|
|
1107
|
+
}
|
|
1108
|
+
// 드래그가 방금 끝났음을 표시
|
|
1109
|
+
justFinishedDraggingRef.current = true;
|
|
1110
|
+
console.log(' - justFinishedDraggingRef.current = true');
|
|
1111
|
+
// 50ms 후 플래그 해제 (클릭 이벤트가 처리된 후)
|
|
1112
|
+
setTimeout(() => {
|
|
1113
|
+
justFinishedDraggingRef.current = false;
|
|
1114
|
+
console.log(' - justFinishedDraggingRef.current = false (타이머)');
|
|
1115
|
+
}, 50);
|
|
1116
|
+
}
|
|
1117
|
+
// 마우스 다운 상태 해제 (가장 중요!)
|
|
1118
|
+
isMouseDownRef.current = false;
|
|
1119
|
+
console.log(' - isMouseDownRef.current = false');
|
|
1120
|
+
// 드래그 선택 모드 무조건 종료 (선택된 셀은 유지)
|
|
1121
|
+
isSelectingCellsRef.current = false;
|
|
1122
|
+
setIsSelectingCells(false);
|
|
1123
|
+
console.log(' - 드래그 모드 종료, 선택 상태는 유지해야 함');
|
|
1124
|
+
// selectionStartCell은 유지하여 선택 상태 보존
|
|
1125
|
+
}, []);
|
|
1126
|
+
// 셀 선택 해제
|
|
1127
|
+
const clearCellSelection = () => {
|
|
1128
|
+
console.log('🔴 clearCellSelection - 셀 선택 해제 호출됨');
|
|
1129
|
+
console.log(' - 해제할 셀 수:', selectedTableCells.length);
|
|
1130
|
+
console.trace(' - 호출 스택:');
|
|
1131
|
+
selectedTableCells.forEach(cell => cell.classList.remove('selected-cell'));
|
|
1132
|
+
setSelectedTableCells([]);
|
|
1133
|
+
setSelectionStartCell(null);
|
|
1134
|
+
};
|
|
1135
|
+
// 표 셀 우클릭 이벤트 처리
|
|
1136
|
+
const handleEditorContextMenu = (e) => {
|
|
1137
|
+
const target = e.target;
|
|
1138
|
+
// 표 셀 우클릭 감지 (td 또는 td 내부 요소)
|
|
1139
|
+
const cell = target.closest('td');
|
|
1140
|
+
if (cell && editorRef.current?.contains(cell)) {
|
|
1141
|
+
e.preventDefault();
|
|
1142
|
+
e.stopPropagation();
|
|
1143
|
+
// 선택된 셀이 없거나, 우클릭한 셀이 선택 영역에 포함되지 않은 경우
|
|
1144
|
+
if (selectedTableCells.length === 0 || !selectedTableCells.includes(cell)) {
|
|
1145
|
+
clearCellSelection();
|
|
1146
|
+
setSelectedTableCell(cell);
|
|
1147
|
+
}
|
|
1148
|
+
else {
|
|
1149
|
+
// 선택된 셀들 중 하나를 우클릭한 경우, 첫 번째 셀을 대표로 사용
|
|
1150
|
+
setSelectedTableCell(selectedTableCells[0]);
|
|
1151
|
+
}
|
|
1152
|
+
setTableContextMenuPosition({ x: e.clientX, y: e.clientY });
|
|
1153
|
+
setIsTableContextMenuOpen(true);
|
|
1154
|
+
setIsTableCellColorOpen(false);
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
932
1157
|
// 링크 수정
|
|
933
1158
|
const updateLink = () => {
|
|
934
1159
|
if (selectedLinkElement && editLinkUrl) {
|
|
@@ -1401,6 +1626,274 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
1401
1626
|
handleInput();
|
|
1402
1627
|
};
|
|
1403
1628
|
// YouTube 삽입
|
|
1629
|
+
// 표 삽입 함수
|
|
1630
|
+
const insertTable = (rows, cols) => {
|
|
1631
|
+
if (rows === 0 || cols === 0)
|
|
1632
|
+
return;
|
|
1633
|
+
// 표 HTML 생성
|
|
1634
|
+
const table = document.createElement('table');
|
|
1635
|
+
table.style.borderCollapse = 'collapse';
|
|
1636
|
+
table.style.width = '100%';
|
|
1637
|
+
table.style.margin = '10px 0';
|
|
1638
|
+
table.setAttribute('border', '1');
|
|
1639
|
+
table.style.border = '1px solid #ddd';
|
|
1640
|
+
const tbody = document.createElement('tbody');
|
|
1641
|
+
for (let i = 0; i < rows; i++) {
|
|
1642
|
+
const tr = document.createElement('tr');
|
|
1643
|
+
for (let j = 0; j < cols; j++) {
|
|
1644
|
+
const td = document.createElement('td');
|
|
1645
|
+
td.style.border = '1px solid #ddd';
|
|
1646
|
+
td.style.padding = '8px';
|
|
1647
|
+
td.style.minWidth = '50px';
|
|
1648
|
+
td.innerHTML = '<br>';
|
|
1649
|
+
tr.appendChild(td);
|
|
1650
|
+
}
|
|
1651
|
+
tbody.appendChild(tr);
|
|
1652
|
+
}
|
|
1653
|
+
table.appendChild(tbody);
|
|
1654
|
+
// 에디터에 포커스 설정
|
|
1655
|
+
if (editorRef.current) {
|
|
1656
|
+
editorRef.current.focus();
|
|
1657
|
+
const selection = window.getSelection();
|
|
1658
|
+
// 저장된 선택 영역이 있으면 복원
|
|
1659
|
+
if (savedTableSelection && selection) {
|
|
1660
|
+
try {
|
|
1661
|
+
selection.removeAllRanges();
|
|
1662
|
+
selection.addRange(savedTableSelection);
|
|
1663
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1664
|
+
}
|
|
1665
|
+
catch (e) {
|
|
1666
|
+
// 선택 영역 복원 실패 시 무시
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
// 선택 영역 재확인
|
|
1670
|
+
if (!selection || selection.rangeCount === 0 || !editorRef.current.contains(selection.anchorNode)) {
|
|
1671
|
+
// 에디터가 비어있으면 p 태그 추가
|
|
1672
|
+
if (!editorRef.current.innerHTML || editorRef.current.innerHTML === '<br>') {
|
|
1673
|
+
const p = document.createElement('p');
|
|
1674
|
+
p.innerHTML = '<br>';
|
|
1675
|
+
editorRef.current.appendChild(p);
|
|
1676
|
+
}
|
|
1677
|
+
// 커서를 에디터 끝으로 이동
|
|
1678
|
+
const range = document.createRange();
|
|
1679
|
+
range.selectNodeContents(editorRef.current);
|
|
1680
|
+
range.collapse(false);
|
|
1681
|
+
selection?.removeAllRanges();
|
|
1682
|
+
selection?.addRange(range);
|
|
1683
|
+
}
|
|
1684
|
+
// 표 삽입
|
|
1685
|
+
if (selection && selection.rangeCount > 0) {
|
|
1686
|
+
const range = selection.getRangeAt(0);
|
|
1687
|
+
range.deleteContents();
|
|
1688
|
+
range.insertNode(table);
|
|
1689
|
+
// 표 다음에 새 문단 추가
|
|
1690
|
+
const newP = document.createElement('p');
|
|
1691
|
+
newP.innerHTML = '<br>';
|
|
1692
|
+
table.after(newP);
|
|
1693
|
+
// 커서를 첫 번째 셀로 이동
|
|
1694
|
+
const firstCell = table.querySelector('td');
|
|
1695
|
+
if (firstCell) {
|
|
1696
|
+
const newRange = document.createRange();
|
|
1697
|
+
newRange.selectNodeContents(firstCell);
|
|
1698
|
+
newRange.collapse(true);
|
|
1699
|
+
selection.removeAllRanges();
|
|
1700
|
+
selection.addRange(newRange);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
else {
|
|
1704
|
+
// 폴백: 에디터 끝에 추가
|
|
1705
|
+
editorRef.current.appendChild(table);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
// 상태 초기화
|
|
1709
|
+
setIsTableDropdownOpen(false);
|
|
1710
|
+
setTableRows(0);
|
|
1711
|
+
setTableCols(0);
|
|
1712
|
+
setSavedTableSelection(null);
|
|
1713
|
+
editorRef.current?.focus();
|
|
1714
|
+
handleInput();
|
|
1715
|
+
};
|
|
1716
|
+
// 다중 셀 선택 범위 계산
|
|
1717
|
+
const getCellsInRange = (startCell, endCell) => {
|
|
1718
|
+
const table = startCell.closest('table');
|
|
1719
|
+
if (!table)
|
|
1720
|
+
return [];
|
|
1721
|
+
const tbody = table.querySelector('tbody');
|
|
1722
|
+
if (!tbody)
|
|
1723
|
+
return [];
|
|
1724
|
+
const startRow = startCell.parentElement;
|
|
1725
|
+
const endRow = endCell.parentElement;
|
|
1726
|
+
const startRowIndex = Array.from(tbody.rows).indexOf(startRow);
|
|
1727
|
+
const endRowIndex = Array.from(tbody.rows).indexOf(endRow);
|
|
1728
|
+
const startColIndex = startCell.cellIndex;
|
|
1729
|
+
const endColIndex = endCell.cellIndex;
|
|
1730
|
+
const minRow = Math.min(startRowIndex, endRowIndex);
|
|
1731
|
+
const maxRow = Math.max(startRowIndex, endRowIndex);
|
|
1732
|
+
const minCol = Math.min(startColIndex, endColIndex);
|
|
1733
|
+
const maxCol = Math.max(startColIndex, endColIndex);
|
|
1734
|
+
const cells = [];
|
|
1735
|
+
for (let r = minRow; r <= maxRow; r++) {
|
|
1736
|
+
const row = tbody.rows[r];
|
|
1737
|
+
for (let c = minCol; c <= maxCol; c++) {
|
|
1738
|
+
if (row.cells[c]) {
|
|
1739
|
+
cells.push(row.cells[c]);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
return cells;
|
|
1744
|
+
};
|
|
1745
|
+
// 표 셀 배경색 변경 (단일/다중)
|
|
1746
|
+
const changeTableCellBackgroundColor = (color) => {
|
|
1747
|
+
// 다중 셀이 선택되어 있으면 모든 선택된 셀에 적용
|
|
1748
|
+
if (selectedTableCells.length > 0) {
|
|
1749
|
+
selectedTableCells.forEach(cell => {
|
|
1750
|
+
cell.style.backgroundColor = color;
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
else if (selectedTableCell) {
|
|
1754
|
+
// 단일 셀에만 적용
|
|
1755
|
+
selectedTableCell.style.backgroundColor = color;
|
|
1756
|
+
}
|
|
1757
|
+
setIsTableCellColorOpen(false);
|
|
1758
|
+
handleInput();
|
|
1759
|
+
};
|
|
1760
|
+
// 셀 배경색 초기화
|
|
1761
|
+
const resetTableCellBackgroundColor = () => {
|
|
1762
|
+
if (selectedTableCells.length > 0) {
|
|
1763
|
+
selectedTableCells.forEach(cell => {
|
|
1764
|
+
cell.style.backgroundColor = '';
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
else if (selectedTableCell) {
|
|
1768
|
+
selectedTableCell.style.backgroundColor = '';
|
|
1769
|
+
}
|
|
1770
|
+
setIsTableCellColorOpen(false);
|
|
1771
|
+
handleInput();
|
|
1772
|
+
};
|
|
1773
|
+
// 셀 정렬 설정
|
|
1774
|
+
const changeTableCellAlign = (align) => {
|
|
1775
|
+
if (selectedTableCells.length > 0) {
|
|
1776
|
+
selectedTableCells.forEach(cell => {
|
|
1777
|
+
cell.style.textAlign = align;
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
else if (selectedTableCell) {
|
|
1781
|
+
selectedTableCell.style.textAlign = align;
|
|
1782
|
+
}
|
|
1783
|
+
handleInput();
|
|
1784
|
+
};
|
|
1785
|
+
// 행 추가 (위/아래)
|
|
1786
|
+
const addTableRow = (position) => {
|
|
1787
|
+
if (!selectedTableCell)
|
|
1788
|
+
return;
|
|
1789
|
+
const row = selectedTableCell.closest('tr');
|
|
1790
|
+
if (!row)
|
|
1791
|
+
return;
|
|
1792
|
+
const table = row.closest('table');
|
|
1793
|
+
if (!table)
|
|
1794
|
+
return;
|
|
1795
|
+
const newRow = document.createElement('tr');
|
|
1796
|
+
const cellCount = row.cells.length;
|
|
1797
|
+
for (let i = 0; i < cellCount; i++) {
|
|
1798
|
+
const td = document.createElement('td');
|
|
1799
|
+
td.style.border = '1px solid #ddd';
|
|
1800
|
+
td.style.padding = '8px';
|
|
1801
|
+
td.style.minWidth = '50px';
|
|
1802
|
+
td.innerHTML = '<br>';
|
|
1803
|
+
newRow.appendChild(td);
|
|
1804
|
+
}
|
|
1805
|
+
if (position === 'above') {
|
|
1806
|
+
row.parentNode?.insertBefore(newRow, row);
|
|
1807
|
+
}
|
|
1808
|
+
else {
|
|
1809
|
+
row.parentNode?.insertBefore(newRow, row.nextSibling);
|
|
1810
|
+
}
|
|
1811
|
+
setIsTableContextMenuOpen(false);
|
|
1812
|
+
handleInput();
|
|
1813
|
+
};
|
|
1814
|
+
// 행 삭제
|
|
1815
|
+
const deleteTableRow = () => {
|
|
1816
|
+
if (!selectedTableCell)
|
|
1817
|
+
return;
|
|
1818
|
+
const row = selectedTableCell.closest('tr');
|
|
1819
|
+
if (!row)
|
|
1820
|
+
return;
|
|
1821
|
+
const tbody = row.parentNode;
|
|
1822
|
+
if (!tbody)
|
|
1823
|
+
return;
|
|
1824
|
+
// 마지막 행이면 삭제 불가
|
|
1825
|
+
if (tbody.rows.length <= 1) {
|
|
1826
|
+
alert('표에는 최소 1개의 행이 필요합니다.');
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
row.remove();
|
|
1830
|
+
setIsTableContextMenuOpen(false);
|
|
1831
|
+
setSelectedTableCell(null);
|
|
1832
|
+
handleInput();
|
|
1833
|
+
};
|
|
1834
|
+
// 열 추가 (좌/우)
|
|
1835
|
+
const addTableColumn = (position) => {
|
|
1836
|
+
if (!selectedTableCell)
|
|
1837
|
+
return;
|
|
1838
|
+
const cellIndex = selectedTableCell.cellIndex;
|
|
1839
|
+
const row = selectedTableCell.closest('tr');
|
|
1840
|
+
if (!row)
|
|
1841
|
+
return;
|
|
1842
|
+
const table = row.closest('table');
|
|
1843
|
+
if (!table)
|
|
1844
|
+
return;
|
|
1845
|
+
const tbody = table.querySelector('tbody');
|
|
1846
|
+
if (!tbody)
|
|
1847
|
+
return;
|
|
1848
|
+
Array.from(tbody.rows).forEach(row => {
|
|
1849
|
+
const newCell = document.createElement('td');
|
|
1850
|
+
newCell.style.border = '1px solid #ddd';
|
|
1851
|
+
newCell.style.padding = '8px';
|
|
1852
|
+
newCell.style.minWidth = '50px';
|
|
1853
|
+
newCell.innerHTML = '<br>';
|
|
1854
|
+
if (position === 'left') {
|
|
1855
|
+
row.insertBefore(newCell, row.cells[cellIndex]);
|
|
1856
|
+
}
|
|
1857
|
+
else {
|
|
1858
|
+
if (cellIndex + 1 < row.cells.length) {
|
|
1859
|
+
row.insertBefore(newCell, row.cells[cellIndex + 1]);
|
|
1860
|
+
}
|
|
1861
|
+
else {
|
|
1862
|
+
row.appendChild(newCell);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
});
|
|
1866
|
+
setIsTableContextMenuOpen(false);
|
|
1867
|
+
handleInput();
|
|
1868
|
+
};
|
|
1869
|
+
// 열 삭제
|
|
1870
|
+
const deleteTableColumn = () => {
|
|
1871
|
+
if (!selectedTableCell)
|
|
1872
|
+
return;
|
|
1873
|
+
const cellIndex = selectedTableCell.cellIndex;
|
|
1874
|
+
const row = selectedTableCell.closest('tr');
|
|
1875
|
+
if (!row)
|
|
1876
|
+
return;
|
|
1877
|
+
const table = row.closest('table');
|
|
1878
|
+
if (!table)
|
|
1879
|
+
return;
|
|
1880
|
+
const tbody = table.querySelector('tbody');
|
|
1881
|
+
if (!tbody)
|
|
1882
|
+
return;
|
|
1883
|
+
// 마지막 열이면 삭제 불가
|
|
1884
|
+
if (row.cells.length <= 1) {
|
|
1885
|
+
alert('표에는 최소 1개의 열이 필요합니다.');
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
Array.from(tbody.rows).forEach(row => {
|
|
1889
|
+
if (row.cells[cellIndex]) {
|
|
1890
|
+
row.cells[cellIndex].remove();
|
|
1891
|
+
}
|
|
1892
|
+
});
|
|
1893
|
+
setIsTableContextMenuOpen(false);
|
|
1894
|
+
setSelectedTableCell(null);
|
|
1895
|
+
handleInput();
|
|
1896
|
+
};
|
|
1404
1897
|
const insertYoutube = () => {
|
|
1405
1898
|
if (!youtubeUrl)
|
|
1406
1899
|
return;
|
|
@@ -1645,11 +2138,116 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
1645
2138
|
return;
|
|
1646
2139
|
}
|
|
1647
2140
|
const range = selection.getRangeAt(0);
|
|
1648
|
-
//
|
|
2141
|
+
// 선택 영역에 포함된 모든 표 셀 찾기
|
|
2142
|
+
const getSelectedTableCells = () => {
|
|
2143
|
+
const cells = [];
|
|
2144
|
+
const container = range.commonAncestorContainer;
|
|
2145
|
+
// 컨테이너가 표인지 확인
|
|
2146
|
+
let tableElement = null;
|
|
2147
|
+
let current = container.nodeType === Node.TEXT_NODE ? container.parentElement : container;
|
|
2148
|
+
while (current && current !== editorRef.current) {
|
|
2149
|
+
if (current.tagName === 'TABLE' || current.tagName === 'TBODY' || current.tagName === 'TR') {
|
|
2150
|
+
// 상위 table 요소 찾기
|
|
2151
|
+
let table = current;
|
|
2152
|
+
while (table && table.tagName !== 'TABLE') {
|
|
2153
|
+
table = table.parentElement;
|
|
2154
|
+
}
|
|
2155
|
+
tableElement = table;
|
|
2156
|
+
break;
|
|
2157
|
+
}
|
|
2158
|
+
current = current.parentElement;
|
|
2159
|
+
}
|
|
2160
|
+
if (!tableElement)
|
|
2161
|
+
return cells;
|
|
2162
|
+
// 표 내의 모든 셀 확인
|
|
2163
|
+
const allCells = tableElement.querySelectorAll('td, th');
|
|
2164
|
+
allCells.forEach(cell => {
|
|
2165
|
+
if (range.intersectsNode(cell)) {
|
|
2166
|
+
cells.push(cell);
|
|
2167
|
+
}
|
|
2168
|
+
});
|
|
2169
|
+
return cells;
|
|
2170
|
+
};
|
|
2171
|
+
const selectedCells = getSelectedTableCells();
|
|
2172
|
+
// 여러 표 셀이 선택된 경우
|
|
2173
|
+
if (selectedCells.length > 1) {
|
|
2174
|
+
selectedCells.forEach(cell => {
|
|
2175
|
+
// 각 셀의 모든 내용을 span으로 감싸기
|
|
2176
|
+
const cellContents = Array.from(cell.childNodes);
|
|
2177
|
+
cellContents.forEach(node => {
|
|
2178
|
+
if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim()) {
|
|
2179
|
+
// 텍스트 노드를 span으로 감싸기
|
|
2180
|
+
const span = document.createElement('span');
|
|
2181
|
+
if (styleProperty === 'color') {
|
|
2182
|
+
span.style.color = color;
|
|
2183
|
+
}
|
|
2184
|
+
else if (styleProperty === 'background-color') {
|
|
2185
|
+
span.style.backgroundColor = color;
|
|
2186
|
+
}
|
|
2187
|
+
span.textContent = node.textContent;
|
|
2188
|
+
cell.replaceChild(span, node);
|
|
2189
|
+
}
|
|
2190
|
+
else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
2191
|
+
// 기존 요소에 스타일 적용
|
|
2192
|
+
const element = node;
|
|
2193
|
+
if (styleProperty === 'color') {
|
|
2194
|
+
element.style.color = color;
|
|
2195
|
+
}
|
|
2196
|
+
else if (styleProperty === 'background-color') {
|
|
2197
|
+
element.style.backgroundColor = color;
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
});
|
|
2201
|
+
});
|
|
2202
|
+
// 선택 해제
|
|
2203
|
+
selection.removeAllRanges();
|
|
2204
|
+
editorRef.current?.focus();
|
|
2205
|
+
handleInput();
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
// 단일 셀 내부 또는 일반 텍스트
|
|
2209
|
+
const commonAncestor = range.commonAncestorContainer;
|
|
2210
|
+
// 선택 영역이 표 셀 내부인지 확인
|
|
2211
|
+
const isInTableCell = (node) => {
|
|
2212
|
+
let current = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
|
|
2213
|
+
while (current && current !== editorRef.current) {
|
|
2214
|
+
if (current.tagName === 'TD' || current.tagName === 'TH') {
|
|
2215
|
+
return true;
|
|
2216
|
+
}
|
|
2217
|
+
current = current.parentElement;
|
|
2218
|
+
}
|
|
2219
|
+
return false;
|
|
2220
|
+
};
|
|
2221
|
+
// 표 셀 내부에서의 색상 변경 (단일 셀)
|
|
2222
|
+
if (isInTableCell(commonAncestor)) {
|
|
2223
|
+
try {
|
|
2224
|
+
const contents = range.extractContents();
|
|
2225
|
+
const span = document.createElement('span');
|
|
2226
|
+
if (styleProperty === 'color') {
|
|
2227
|
+
span.style.color = color;
|
|
2228
|
+
}
|
|
2229
|
+
else if (styleProperty === 'background-color') {
|
|
2230
|
+
span.style.backgroundColor = color;
|
|
2231
|
+
}
|
|
2232
|
+
span.appendChild(contents);
|
|
2233
|
+
range.insertNode(span);
|
|
2234
|
+
// 커서 위치 조정
|
|
2235
|
+
range.setStartAfter(span);
|
|
2236
|
+
range.collapse(true);
|
|
2237
|
+
selection.removeAllRanges();
|
|
2238
|
+
selection.addRange(range);
|
|
2239
|
+
editorRef.current?.focus();
|
|
2240
|
+
handleInput();
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
catch (error) {
|
|
2244
|
+
console.error('표 셀 내부 색상 변경 오류:', error);
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
// 일반 텍스트에 대한 색상 변경
|
|
1649
2248
|
const span = document.createElement('span');
|
|
1650
2249
|
try {
|
|
1651
2250
|
const contents = range.extractContents();
|
|
1652
|
-
// 스타일 적용 - setAttribute를 사용하여 !important 포함
|
|
1653
2251
|
if (styleProperty === 'color') {
|
|
1654
2252
|
span.setAttribute('style', `color: ${color} !important;`);
|
|
1655
2253
|
}
|
|
@@ -1658,14 +2256,12 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
1658
2256
|
}
|
|
1659
2257
|
span.appendChild(contents);
|
|
1660
2258
|
range.insertNode(span);
|
|
1661
|
-
// 커서 위치 조정
|
|
1662
2259
|
range.selectNodeContents(span);
|
|
1663
2260
|
range.collapse(false);
|
|
1664
2261
|
selection.removeAllRanges();
|
|
1665
2262
|
selection.addRange(range);
|
|
1666
2263
|
}
|
|
1667
2264
|
catch {
|
|
1668
|
-
// 폴백: execCommand 사용
|
|
1669
2265
|
if (styleProperty === 'color') {
|
|
1670
2266
|
document.execCommand('foreColor', false, color);
|
|
1671
2267
|
}
|
|
@@ -1737,6 +2333,16 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
1737
2333
|
setYoutubeUrl('');
|
|
1738
2334
|
setSavedYoutubeSelection(null);
|
|
1739
2335
|
}
|
|
2336
|
+
// 표 드롭다운 체크
|
|
2337
|
+
const tableDropdown = document.querySelector(`.${styles.tableDropdown}`);
|
|
2338
|
+
if (tableButtonRef.current &&
|
|
2339
|
+
!tableButtonRef.current.contains(target) &&
|
|
2340
|
+
(!tableDropdown || !tableDropdown.contains(target))) {
|
|
2341
|
+
setIsTableDropdownOpen(false);
|
|
2342
|
+
setTableRows(0);
|
|
2343
|
+
setTableCols(0);
|
|
2344
|
+
setSavedTableSelection(null);
|
|
2345
|
+
}
|
|
1740
2346
|
// 이미지 편집 팝업 닫기
|
|
1741
2347
|
if (isImageEditPopupOpen && selectedImage) {
|
|
1742
2348
|
const imageEditPopup = document.querySelector(`.${styles.imageDropdown}`);
|
|
@@ -1758,14 +2364,20 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
1758
2364
|
setEditLinkTarget('_self');
|
|
1759
2365
|
}
|
|
1760
2366
|
}
|
|
2367
|
+
// 표 컨텍스트 메뉴 닫기
|
|
2368
|
+
if (isTableContextMenuOpen && tableContextMenuRef.current && !tableContextMenuRef.current.contains(target)) {
|
|
2369
|
+
setIsTableContextMenuOpen(false);
|
|
2370
|
+
setSelectedTableCell(null);
|
|
2371
|
+
setIsTableCellColorOpen(false);
|
|
2372
|
+
}
|
|
1761
2373
|
};
|
|
1762
|
-
if (isParagraphDropdownOpen || isTextColorOpen || isBgColorOpen || isAlignDropdownOpen || isLinkDropdownOpen || isEditLinkPopupOpen || isImageDropdownOpen || isImageEditPopupOpen || isYoutubeDropdownOpen) {
|
|
2374
|
+
if (isParagraphDropdownOpen || isTextColorOpen || isBgColorOpen || isAlignDropdownOpen || isLinkDropdownOpen || isEditLinkPopupOpen || isImageDropdownOpen || isImageEditPopupOpen || isYoutubeDropdownOpen || isTableDropdownOpen || isTableContextMenuOpen) {
|
|
1763
2375
|
document.addEventListener('mousedown', handleClickOutside);
|
|
1764
2376
|
}
|
|
1765
2377
|
return () => {
|
|
1766
2378
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
1767
2379
|
};
|
|
1768
|
-
}, [isParagraphDropdownOpen, isTextColorOpen, isBgColorOpen, isAlignDropdownOpen, isLinkDropdownOpen, isEditLinkPopupOpen, isImageDropdownOpen, isImageEditPopupOpen, isYoutubeDropdownOpen, selectedLinkElement, selectedImage]);
|
|
2380
|
+
}, [isParagraphDropdownOpen, isTextColorOpen, isBgColorOpen, isAlignDropdownOpen, isLinkDropdownOpen, isEditLinkPopupOpen, isImageDropdownOpen, isImageEditPopupOpen, isYoutubeDropdownOpen, isTableDropdownOpen, isTableContextMenuOpen, selectedLinkElement, selectedImage]);
|
|
1769
2381
|
// 리사이즈 중 마우스 이벤트 처리
|
|
1770
2382
|
useEffect(() => {
|
|
1771
2383
|
if (!isResizing || !resizeStartData)
|
|
@@ -1890,6 +2502,20 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
1890
2502
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
1891
2503
|
};
|
|
1892
2504
|
}, [isResizing, resizeStartData, selectedImage, selectedYoutube]);
|
|
2505
|
+
// 표 셀 드래그 선택 이벤트 등록
|
|
2506
|
+
useEffect(() => {
|
|
2507
|
+
if (!editorRef.current || isCodeView)
|
|
2508
|
+
return;
|
|
2509
|
+
const editor = editorRef.current;
|
|
2510
|
+
editor.addEventListener('mousedown', handleCellMouseDown);
|
|
2511
|
+
document.addEventListener('mousemove', handleCellMouseMove);
|
|
2512
|
+
document.addEventListener('mouseup', handleCellMouseUp);
|
|
2513
|
+
return () => {
|
|
2514
|
+
editor.removeEventListener('mousedown', handleCellMouseDown);
|
|
2515
|
+
document.removeEventListener('mousemove', handleCellMouseMove);
|
|
2516
|
+
document.removeEventListener('mouseup', handleCellMouseUp);
|
|
2517
|
+
};
|
|
2518
|
+
}, [handleCellMouseDown, handleCellMouseMove, handleCellMouseUp, isCodeView]);
|
|
1893
2519
|
// 스크롤, 리사이즈 및 이미지/유튜브 드래그 시 편집창 숨기기
|
|
1894
2520
|
useEffect(() => {
|
|
1895
2521
|
if (!selectedImage && !selectedYoutube)
|
|
@@ -2066,7 +2692,7 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
2066
2692
|
}, 100);
|
|
2067
2693
|
return () => clearTimeout(timer);
|
|
2068
2694
|
}, []);
|
|
2069
|
-
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: {
|
|
2695
|
+
return (_jsxs("div", { className: `${styles.editor} ${statusClass}`, style: { width, position: 'relative' }, children: [_jsxs("div", { className: styles.toolbar, children: [isToolbarItemEnabled('undo-redo') && (_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: {
|
|
2070
2696
|
opacity: historyIndex <= 0 ? 0.65 : 1,
|
|
2071
2697
|
backgroundColor: 'transparent',
|
|
2072
2698
|
border: 'none',
|
|
@@ -2076,7 +2702,7 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
2076
2702
|
backgroundColor: 'transparent',
|
|
2077
2703
|
border: 'none',
|
|
2078
2704
|
cursor: historyIndex >= history.length - 1 ? 'not-allowed' : 'pointer'
|
|
2079
|
-
}, children: _jsx("i", { className: styles.redo }) })] }), _jsxs("div", { className: styles.toolbarGroup, ref: paragraphButtonRef, children: [_jsxs("button", { type: "button", className: styles.paragraphButton, onClick: () => {
|
|
2705
|
+
}, children: _jsx("i", { className: styles.redo }) })] })), isToolbarItemEnabled('paragraph') && (_jsxs("div", { className: styles.toolbarGroup, ref: paragraphButtonRef, children: [_jsxs("button", { type: "button", className: styles.paragraphButton, onClick: () => {
|
|
2080
2706
|
setIsParagraphDropdownOpen(!isParagraphDropdownOpen);
|
|
2081
2707
|
setIsTextColorOpen(false);
|
|
2082
2708
|
setIsBgColorOpen(false);
|
|
@@ -2084,7 +2710,7 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
2084
2710
|
}, title: "\uBB38\uB2E8 \uD615\uC2DD", children: [_jsx("span", { children: getCurrentStyleLabel() }), _jsx("i", { className: styles.dropdownArrow })] }), isParagraphDropdownOpen && (_jsx("div", { className: styles.paragraphDropdown, style: {
|
|
2085
2711
|
top: paragraphButtonRef.current?.getBoundingClientRect().bottom ?? 0,
|
|
2086
2712
|
left: paragraphButtonRef.current?.getBoundingClientRect().left ?? 0
|
|
2087
|
-
}, 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: () => {
|
|
2713
|
+
}, 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))) }))] })), isToolbarItemEnabled('text-style') && (_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 }) })] })), isToolbarItemEnabled('color') && (_jsxs("div", { className: styles.toolbarGroup, children: [_jsxs("div", { ref: textColorButtonRef, style: { position: 'relative' }, children: [_jsx("button", { type: "button", className: styles.toolbarButton, onClick: () => {
|
|
2088
2714
|
const selection = window.getSelection();
|
|
2089
2715
|
if (selection && !selection.isCollapsed) {
|
|
2090
2716
|
// 선택 영역 저장
|
|
@@ -2116,7 +2742,7 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
2116
2742
|
changeBackgroundColor(color, savedSelection);
|
|
2117
2743
|
setIsBgColorOpen(false);
|
|
2118
2744
|
setSavedSelection(null);
|
|
2119
|
-
} }, color))) }, rowIndex))) }))] })] }), _jsxs("div", { className: styles.toolbarGroup, children: [_jsxs("div", { ref: alignButtonRef, style: { position: 'relative' }, children: [_jsx("button", { type: "button", className: styles.toolbarButton, onClick: () => {
|
|
2745
|
+
} }, color))) }, rowIndex))) }))] })] })), (isToolbarItemEnabled('align') || isToolbarItemEnabled('list') || isToolbarItemEnabled('table')) && (_jsxs("div", { className: styles.toolbarGroup, children: [isToolbarItemEnabled('align') && (_jsxs("div", { ref: alignButtonRef, style: { position: 'relative' }, children: [_jsx("button", { type: "button", className: styles.toolbarButton, onClick: () => {
|
|
2120
2746
|
setIsAlignDropdownOpen(!isAlignDropdownOpen);
|
|
2121
2747
|
setIsParagraphDropdownOpen(false);
|
|
2122
2748
|
setIsTextColorOpen(false);
|
|
@@ -2136,7 +2762,22 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
2136
2762
|
}
|
|
2137
2763
|
setCurrentAlign(option.value);
|
|
2138
2764
|
setIsAlignDropdownOpen(false);
|
|
2139
|
-
}, 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 }) })] }),
|
|
2765
|
+
}, title: option.label, children: _jsx("i", { className: styles[option.icon] }) }, option.value))) }))] })), isToolbarItemEnabled('list') && (_jsxs(_Fragment, { children: [_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 }) })] })), isToolbarItemEnabled('table') && (_jsxs("div", { ref: tableButtonRef, style: { position: 'relative' }, children: [_jsx("button", { type: "button", className: styles.toolbarButton, onClick: () => {
|
|
2766
|
+
// 현재 선택 영역 저장
|
|
2767
|
+
const selection = window.getSelection();
|
|
2768
|
+
if (selection && selection.rangeCount > 0) {
|
|
2769
|
+
setSavedTableSelection(selection.getRangeAt(0).cloneRange());
|
|
2770
|
+
}
|
|
2771
|
+
setIsTableDropdownOpen(!isTableDropdownOpen);
|
|
2772
|
+
}, title: "\uD45C \uC0BD\uC785", children: _jsx("i", { className: styles.table }) }), isTableDropdownOpen && (_jsx("div", { className: styles.tableDropdown, style: {
|
|
2773
|
+
top: tableButtonRef.current?.getBoundingClientRect().bottom ?? 0,
|
|
2774
|
+
left: tableButtonRef.current?.getBoundingClientRect().left ?? 0
|
|
2775
|
+
}, children: _jsxs("div", { className: styles.tableGridSelector, children: [_jsx("div", { className: styles.tableGridLabel, children: tableRows > 0 && tableCols > 0 ? `${tableRows} × ${tableCols} 표` : '표 크기 선택' }), _jsx("div", { className: styles.tableGrid, children: Array.from({ length: 10 }, (_, i) => i + 1).map(row => (_jsx("div", { className: styles.tableGridRow, children: Array.from({ length: 10 }, (_, j) => j + 1).map(col => (_jsx("div", { className: `${styles.tableGridCell} ${row <= tableRows && col <= tableCols ? styles.active : ''}`, onMouseEnter: () => {
|
|
2776
|
+
setTableRows(row);
|
|
2777
|
+
setTableCols(col);
|
|
2778
|
+
}, onClick: () => {
|
|
2779
|
+
insertTable(row, col);
|
|
2780
|
+
} }, `${row}-${col}`))) }, row))) })] }) }))] }))] })), (isToolbarItemEnabled('link') || isToolbarItemEnabled('image') || isToolbarItemEnabled('youtube')) && (_jsxs("div", { className: styles.toolbarGroup, children: [isToolbarItemEnabled('link') && (_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: {
|
|
2140
2781
|
top: linkButtonRef.current?.getBoundingClientRect().bottom ?? 0,
|
|
2141
2782
|
left: linkButtonRef.current?.getBoundingClientRect().left ?? 0
|
|
2142
2783
|
}, 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: () => {
|
|
@@ -2144,7 +2785,7 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
2144
2785
|
setLinkUrl('');
|
|
2145
2786
|
setLinkTarget('_blank');
|
|
2146
2787
|
setSavedSelection(null);
|
|
2147
|
-
}, 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: {
|
|
2788
|
+
}, className: styles.default, children: "\uCDE8\uC18C" }), _jsx("button", { type: "button", onClick: applyLink, disabled: !linkUrl, className: styles.primary, children: "\uC0BD\uC785" })] })] }))] })), isToolbarItemEnabled('image') && (_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: {
|
|
2148
2789
|
top: imageButtonRef.current?.getBoundingClientRect().bottom ?? 0,
|
|
2149
2790
|
left: imageButtonRef.current?.getBoundingClientRect().left ?? 0
|
|
2150
2791
|
}, children: [_jsxs("div", { className: styles.imageTabSection, children: [_jsxs("div", { className: styles.imageTabButtons, children: [_jsx("button", { type: "button", className: imageTabMode === 'file' ? styles.active : '', onClick: () => {
|
|
@@ -2166,7 +2807,7 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
2166
2807
|
setImageAlign('left'); // 좌측으로 초기화
|
|
2167
2808
|
setImageAlt('');
|
|
2168
2809
|
setSavedImageSelection(null); // 저장된 선택 영역 초기화
|
|
2169
|
-
}, 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) => {
|
|
2810
|
+
}, className: styles.default, children: "\uCDE8\uC18C" }), _jsx("button", { type: "button", onClick: insertImage, disabled: !imageUrl && !imageFile, className: styles.primary, children: "\uC0BD\uC785" })] })] }))] })), isToolbarItemEnabled('youtube') && (_jsxs("div", { ref: youtubeButtonRef, style: { position: 'relative' }, children: [_jsx("button", { type: "button", className: styles.toolbarButton, onClick: (e) => {
|
|
2170
2811
|
e.stopPropagation();
|
|
2171
2812
|
// 현재 선택 영역 저장
|
|
2172
2813
|
const selection = window.getSelection();
|
|
@@ -2193,7 +2834,7 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
2193
2834
|
setYoutubeWidth('100%');
|
|
2194
2835
|
setYoutubeAlign('center');
|
|
2195
2836
|
setSavedYoutubeSelection(null);
|
|
2196
|
-
}, 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: {
|
|
2837
|
+
}, children: "\uCDE8\uC18C" }), _jsx("button", { type: "button", className: styles.primary, onClick: () => insertYoutube(), disabled: !youtubeUrl, children: "\uC0BD\uC785" })] })] }))] }))] })), isToolbarItemEnabled('format') && (_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 }) }) })), isToolbarItemEnabled('code') && (_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: {
|
|
2197
2838
|
height: height === 'contents' ? 'auto' : (height || '300px'),
|
|
2198
2839
|
minHeight: minHeight || (height === 'contents' ? '100px' : '200px'),
|
|
2199
2840
|
maxHeight: maxHeight || (height === 'contents' ? undefined : undefined),
|
|
@@ -2206,7 +2847,7 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
2206
2847
|
minHeight: height === 'contents' ? 'auto' : 0,
|
|
2207
2848
|
height: height === 'contents' && savedEditorHeight ? `${savedEditorHeight}px` : undefined,
|
|
2208
2849
|
resize: 'none'
|
|
2209
|
-
}, placeholder: placeholder })) : (_jsx("div", { ref: editorRef, id: editorID, className: styles.editorContent, contentEditable: true, onInput: handleInput, onCompositionStart: handleCompositionStart, onCompositionEnd: handleCompositionEnd, onPaste: handlePaste, onClick: handleEditorClick, onKeyUp: () => {
|
|
2850
|
+
}, placeholder: placeholder })) : (_jsx("div", { ref: editorRef, id: editorID, className: styles.editorContent, contentEditable: true, onInput: handleInput, onCompositionStart: handleCompositionStart, onCompositionEnd: handleCompositionEnd, onPaste: handlePaste, onClick: handleEditorClick, onContextMenu: handleEditorContextMenu, onKeyUp: () => {
|
|
2210
2851
|
detectCurrentParagraphStyle();
|
|
2211
2852
|
detectCurrentAlign();
|
|
2212
2853
|
}, onKeyDown: handleKeyDown, style: {
|
|
@@ -2248,6 +2889,11 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
|
|
|
2248
2889
|
minWidth: '360px',
|
|
2249
2890
|
maxWidth: '90%'
|
|
2250
2891
|
}, 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" })] })] })] }));
|
|
2251
|
-
})()
|
|
2892
|
+
})(), isTableContextMenuOpen && selectedTableCell && (_jsxs("div", { ref: tableContextMenuRef, className: styles.tableContextMenu, style: {
|
|
2893
|
+
position: 'fixed',
|
|
2894
|
+
top: tableContextMenuPosition.y,
|
|
2895
|
+
left: tableContextMenuPosition.x,
|
|
2896
|
+
zIndex: 10000
|
|
2897
|
+
}, children: [selectedTableCells.length > 1 && (_jsxs("div", { className: styles.tableContextMenuHeader, children: [selectedTableCells.length, "\uAC1C \uC140 \uC120\uD0DD\uB428"] })), _jsxs("div", { className: styles.tableContextMenuItem, children: [_jsxs("button", { type: "button", onClick: () => setIsTableCellColorOpen(!isTableCellColorOpen), className: styles.tableContextMenuButton, children: ["\uC140 \uBC30\uACBD\uC0C9 ", selectedTableCells.length > 1 ? `(${selectedTableCells.length}개)` : '', _jsx("span", { className: styles.arrow, children: isTableCellColorOpen ? '▲' : '▼' })] }), isTableCellColorOpen && (_jsx("div", { className: styles.colorPaletteInline, children: colorPalette.map((row, rowIndex) => (_jsx("div", { className: styles.colorRow, children: row.map((color, colIndex) => (_jsx("button", { type: "button", className: styles.colorButton, style: { backgroundColor: color }, onClick: () => changeTableCellBackgroundColor(color), title: color }, colIndex))) }, rowIndex))) }))] }), _jsx("button", { type: "button", onClick: resetTableCellBackgroundColor, className: styles.tableContextMenuButton, children: "\uBC30\uACBD\uC0C9 \uCD08\uAE30\uD654" }), _jsx("div", { className: styles.tableContextMenuDivider }), _jsx("button", { type: "button", onClick: () => changeTableCellAlign('left'), className: styles.tableContextMenuButton, children: "\uC67C\uCABD \uC815\uB82C" }), _jsx("button", { type: "button", onClick: () => changeTableCellAlign('center'), className: styles.tableContextMenuButton, children: "\uAC00\uC6B4\uB370 \uC815\uB82C" }), _jsx("button", { type: "button", onClick: () => changeTableCellAlign('right'), className: styles.tableContextMenuButton, children: "\uC624\uB978\uCABD \uC815\uB82C" }), _jsx("div", { className: styles.tableContextMenuDivider }), _jsx("button", { type: "button", onClick: () => addTableRow('above'), className: styles.tableContextMenuButton, children: "\uC704\uC5D0 \uD589 \uCD94\uAC00" }), _jsx("button", { type: "button", onClick: () => addTableRow('below'), className: styles.tableContextMenuButton, children: "\uC544\uB798\uC5D0 \uD589 \uCD94\uAC00" }), _jsx("button", { type: "button", onClick: deleteTableRow, className: styles.tableContextMenuButton, children: "\uD589 \uC0AD\uC81C" }), _jsx("div", { className: styles.tableContextMenuDivider }), _jsx("button", { type: "button", onClick: () => addTableColumn('left'), className: styles.tableContextMenuButton, children: "\uC67C\uCABD\uC5D0 \uC5F4 \uCD94\uAC00" }), _jsx("button", { type: "button", onClick: () => addTableColumn('right'), className: styles.tableContextMenuButton, children: "\uC624\uB978\uCABD\uC5D0 \uC5F4 \uCD94\uAC00" }), _jsx("button", { type: "button", onClick: deleteTableColumn, className: styles.tableContextMenuButton, children: "\uC5F4 \uC0AD\uC81C" })] }))] }));
|
|
2252
2898
|
};
|
|
2253
2899
|
export default Editor;
|
|
@@ -214,6 +214,11 @@
|
|
|
214
214
|
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M392.8 1.2c-17-4.9-34.7 5-39.6 22l-128 448c-4.9 17 5 34.7 22 39.6s34.7-5 39.6-22l128-448c4.9-17-5-34.7-22-39.6zm80.6 120.1c-12.5 12.5-12.5 32.8 0 45.3L562.7 256l-89.4 89.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l112-112c12.5-12.5 12.5-32.8 0-45.3l-112-112c-12.5-12.5-32.8-12.5-45.3 0zm-306.7 0c-12.5-12.5-32.8-12.5-45.3 0l-112 112c-12.5 12.5-12.5 32.8 0 45.3l112 112c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256l89.4-89.4c12.5-12.5 12.5-32.8 0-45.3z"/></svg>');
|
|
215
215
|
-webkit-mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M392.8 1.2c-17-4.9-34.7 5-39.6 22l-128 448c-4.9 17 5 34.7 22 39.6s34.7-5 39.6-22l128-448c4.9-17-5-34.7-22-39.6zm80.6 120.1c-12.5 12.5-12.5 32.8 0 45.3L562.7 256l-89.4 89.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l112-112c12.5-12.5 12.5-32.8 0-45.3l-112-112c-12.5-12.5-32.8-12.5-45.3 0zm-306.7 0c-12.5-12.5-32.8-12.5-45.3 0l-112 112c-12.5 12.5-12.5 32.8 0 45.3l112 112c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256l89.4-89.4c12.5-12.5 12.5-32.8 0-45.3z"/></svg>');
|
|
216
216
|
}
|
|
217
|
+
|
|
218
|
+
&.table {
|
|
219
|
+
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M64 256V160H224v96H64zm0 64H224v96H64V320zm224 96V320H448v96H288zM448 256H288V160H448v96zM64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64z"/></svg>');
|
|
220
|
+
-webkit-mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M64 256V160H224v96H64zm0 64H224v96H64V320zm224 96V320H448v96H288zM448 256H288V160H448v96zM64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64z"/></svg>');
|
|
221
|
+
}
|
|
217
222
|
}
|
|
218
223
|
|
|
219
224
|
&:hover i {
|
|
@@ -499,6 +504,127 @@
|
|
|
499
504
|
}
|
|
500
505
|
}
|
|
501
506
|
|
|
507
|
+
// 표 드롭다운
|
|
508
|
+
.tableDropdown {
|
|
509
|
+
position: fixed;
|
|
510
|
+
margin-top: 2px;
|
|
511
|
+
padding: 12px;
|
|
512
|
+
background: color(bg-modal);
|
|
513
|
+
border: 1px solid color(border);
|
|
514
|
+
border-radius: r(2);
|
|
515
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
516
|
+
z-index: 10000;
|
|
517
|
+
min-width: 240px;
|
|
518
|
+
|
|
519
|
+
.tableGridSelector {
|
|
520
|
+
.tableGridLabel {
|
|
521
|
+
text-align: center;
|
|
522
|
+
font-size: 13px;
|
|
523
|
+
color: color(text-body);
|
|
524
|
+
margin-bottom: 8px;
|
|
525
|
+
font-weight: 500;
|
|
526
|
+
min-height: 20px;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.tableGrid {
|
|
530
|
+
display: flex;
|
|
531
|
+
flex-direction: column;
|
|
532
|
+
gap: 2px;
|
|
533
|
+
|
|
534
|
+
.tableGridRow {
|
|
535
|
+
display: flex;
|
|
536
|
+
gap: 2px;
|
|
537
|
+
|
|
538
|
+
.tableGridCell {
|
|
539
|
+
width: 20px;
|
|
540
|
+
height: 20px;
|
|
541
|
+
border: 1px solid color(border);
|
|
542
|
+
background: color(bg-elevation);
|
|
543
|
+
cursor: pointer;
|
|
544
|
+
transition: all 0.15s;
|
|
545
|
+
|
|
546
|
+
&:hover {
|
|
547
|
+
border-color: color(primary);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
&.active {
|
|
551
|
+
background: color(primary-fill);
|
|
552
|
+
border-color: color(primary);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// 표 컨텍스트 메뉴
|
|
561
|
+
.tableContextMenu {
|
|
562
|
+
background: color(bg-modal);
|
|
563
|
+
border: 1px solid color(border);
|
|
564
|
+
border-radius: r(2);
|
|
565
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
566
|
+
padding: 8px 0;
|
|
567
|
+
min-width: 180px;
|
|
568
|
+
|
|
569
|
+
.tableContextMenuHeader {
|
|
570
|
+
padding: 8px 12px;
|
|
571
|
+
font-size: 12px;
|
|
572
|
+
font-weight: 600;
|
|
573
|
+
color: color(primary);
|
|
574
|
+
background: color(primary-fill);
|
|
575
|
+
border-bottom: 1px solid color(border);
|
|
576
|
+
margin-bottom: 4px;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.tableContextMenuItem {
|
|
580
|
+
position: relative;
|
|
581
|
+
|
|
582
|
+
.colorPaletteInline {
|
|
583
|
+
padding: 8px;
|
|
584
|
+
background: color(bg-elevation);
|
|
585
|
+
border-top: 1px solid color(border);
|
|
586
|
+
margin-top: 4px;
|
|
587
|
+
display: flex;
|
|
588
|
+
flex-direction: column;
|
|
589
|
+
gap: 4px;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.tableContextMenuButton {
|
|
594
|
+
width: 100%;
|
|
595
|
+
padding: 8px 12px;
|
|
596
|
+
border: none;
|
|
597
|
+
background: transparent;
|
|
598
|
+
color: color(text-body);
|
|
599
|
+
font-size: 13px;
|
|
600
|
+
cursor: pointer;
|
|
601
|
+
text-align: left;
|
|
602
|
+
transition: all 0.2s;
|
|
603
|
+
display: flex;
|
|
604
|
+
align-items: center;
|
|
605
|
+
justify-content: space-between;
|
|
606
|
+
|
|
607
|
+
&:hover {
|
|
608
|
+
background: color(bg-elevation);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
&:active {
|
|
612
|
+
background: color(default-pressed);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.arrow {
|
|
616
|
+
font-size: 10px;
|
|
617
|
+
color: color(text-sub);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.tableContextMenuDivider {
|
|
622
|
+
height: 1px;
|
|
623
|
+
background: color(border);
|
|
624
|
+
margin: 4px 0;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
502
628
|
// 색상 팔레트 드롭다운
|
|
503
629
|
.colorPalette {
|
|
504
630
|
position: fixed;
|
|
@@ -1239,3 +1365,12 @@
|
|
|
1239
1365
|
width: 100%;
|
|
1240
1366
|
box-sizing: border-box;
|
|
1241
1367
|
}
|
|
1368
|
+
|
|
1369
|
+
// 선택된 표 셀 스타일
|
|
1370
|
+
.editorContent {
|
|
1371
|
+
:global(.selected-cell) {
|
|
1372
|
+
outline: 2px solid color(primary) !important;
|
|
1373
|
+
outline-offset: -2px !important;
|
|
1374
|
+
position: relative;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "podo-ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": "hada0127 <work@tarucy.net>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -60,10 +60,7 @@
|
|
|
60
60
|
"scripts": {
|
|
61
61
|
"dev": "next dev",
|
|
62
62
|
"build": "next build",
|
|
63
|
-
"build:lib": "
|
|
64
|
-
"copy:styles": "find react -name '*.scss' -exec sh -c 'mkdir -p \"dist/$(dirname \"{}\")\" && cp \"{}\" \"dist/{}\"' \\;",
|
|
65
|
-
"fix:scss-paths": "find dist/react -name '*.scss' -type f -exec sed -i '' 's|../../scss|../../../scss|g' {} \\;",
|
|
66
|
-
"clean": "rm -rf dist && rm -f .tsbuildinfo",
|
|
63
|
+
"build:lib": "node ./cli/build-lib.js",
|
|
67
64
|
"prepublishOnly": "npm run build:lib",
|
|
68
65
|
"start": "next start",
|
|
69
66
|
"lint": "next lint",
|
|
@@ -83,6 +80,7 @@
|
|
|
83
80
|
"devDependencies": {
|
|
84
81
|
"@cloudflare/next-on-pages": "^1.13.8",
|
|
85
82
|
"@eslint/js": "^9.8.0",
|
|
83
|
+
"@playwright/test": "^1.56.1",
|
|
86
84
|
"@types/node": "22.10.2",
|
|
87
85
|
"@types/react": "^18.3.3",
|
|
88
86
|
"@types/react-dom": "^18.3.0",
|
|
@@ -90,6 +88,7 @@
|
|
|
90
88
|
"eslint": "^9.8.0",
|
|
91
89
|
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
|
92
90
|
"eslint-plugin-react-refresh": "^0.4.9",
|
|
91
|
+
"glob": "^11.0.3",
|
|
93
92
|
"globals": "^15.9.0",
|
|
94
93
|
"opentype.js": "^1.3.4",
|
|
95
94
|
"prettier": "^3.3.3",
|