podo-ui 0.3.14 → 0.3.16

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 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;CACtB;AAED,QAAA,MAAM,MAAM,iGAUT,WAAW,4CAuxGb,CAAC;AAEF,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,4CAssIb,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', 'HR',
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
- allowedStyles.forEach(styleName => {
389
- const value = element.style.getPropertyValue(styleName);
390
- if (value) {
391
- newElement.style.setProperty(styleName, value);
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 => {
@@ -434,13 +503,61 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
434
503
  }
435
504
  }
436
505
  else if (text) {
437
- // HTML이 없으면 일반 텍스트 삽입
506
+ // HTML이 없으면 일반 텍스트 삽입 (줄바꿈을 <br> 및 <p> 태그로 변환)
438
507
  const selection = window.getSelection();
439
508
  if (selection && selection.rangeCount > 0) {
440
509
  const range = selection.getRangeAt(0);
441
510
  range.deleteContents();
442
- range.insertNode(document.createTextNode(text));
511
+ // 텍스트를 줄 단위로 분리 (연속된 줄바꿈은 문단 구분으로 처리)
512
+ const lines = text.split('\n');
513
+ const paragraphs = [];
514
+ let currentParagraph = [];
515
+ lines.forEach((line) => {
516
+ if (line.trim() === '') {
517
+ // 빈 줄이면 현재 문단을 저장하고 새 문단 시작
518
+ if (currentParagraph.length > 0) {
519
+ paragraphs.push(currentParagraph);
520
+ currentParagraph = [];
521
+ }
522
+ }
523
+ else {
524
+ currentParagraph.push(line);
525
+ }
526
+ });
527
+ // 마지막 문단 추가
528
+ if (currentParagraph.length > 0) {
529
+ paragraphs.push(currentParagraph);
530
+ }
531
+ // 문단이 없으면 빈 문자열 처리
532
+ if (paragraphs.length === 0) {
533
+ range.insertNode(document.createTextNode(text));
534
+ range.collapse(false);
535
+ return;
536
+ }
537
+ const fragment = document.createDocumentFragment();
538
+ paragraphs.forEach((paragraph) => {
539
+ if (paragraph.length === 1) {
540
+ // 한 줄짜리 문단은 <p> 태그로 감싸기
541
+ const p = document.createElement('p');
542
+ p.textContent = paragraph[0];
543
+ fragment.appendChild(p);
544
+ }
545
+ else if (paragraph.length > 1) {
546
+ // 여러 줄은 <p> 태그로 감싸고 내부는 <br>로 구분
547
+ const p = document.createElement('p');
548
+ paragraph.forEach((line, lIndex) => {
549
+ p.appendChild(document.createTextNode(line));
550
+ if (lIndex < paragraph.length - 1) {
551
+ p.appendChild(document.createElement('br'));
552
+ }
553
+ });
554
+ fragment.appendChild(p);
555
+ }
556
+ });
557
+ range.insertNode(fragment);
443
558
  range.collapse(false);
559
+ selection.removeAllRanges();
560
+ selection.addRange(range);
444
561
  }
445
562
  }
446
563
  // 변경사항 반영
@@ -484,6 +601,10 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
484
601
  }
485
602
  else {
486
603
  // 일반 모드에서 코드보기로 전환
604
+ // 셀 선택 상태 해제
605
+ if (selectedTableCells.length > 0) {
606
+ clearCellSelection();
607
+ }
487
608
  if (editorRef.current) {
488
609
  // height가 contents일 때 현재 에디터 높이 저장
489
610
  if (height === 'contents') {
@@ -914,6 +1035,22 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
914
1035
  if (selectedYoutube && !target.closest('.youtube-wrapper') && !isResizing) {
915
1036
  deselectYoutube();
916
1037
  }
1038
+ // 표 컨텍스트 메뉴 닫기
1039
+ if (isTableContextMenuOpen && !target.closest(`.${styles.tableContextMenu}`)) {
1040
+ setIsTableContextMenuOpen(false);
1041
+ setSelectedTableCell(null);
1042
+ setIsTableCellColorOpen(false);
1043
+ }
1044
+ // 표 셀 클릭 시에는 선택 유지
1045
+ const clickedCell = target.closest('td');
1046
+ // 드래그가 방금 끝난 경우 선택 해제하지 않음
1047
+ if (justFinishedDraggingRef.current) {
1048
+ return;
1049
+ }
1050
+ // 표 셀 외부를 클릭한 경우에만 선택 해제
1051
+ if (!clickedCell && selectedTableCells.length > 0) {
1052
+ clearCellSelection();
1053
+ }
917
1054
  // 링크 요소인지 확인
918
1055
  const linkElement = target.closest('a');
919
1056
  if (linkElement && editorRef.current?.contains(linkElement)) {
@@ -929,6 +1066,112 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
929
1066
  detectCurrentAlign();
930
1067
  }
931
1068
  };
1069
+ // 표 셀 마우스 다운 (드래그 선택 시작)
1070
+ const handleCellMouseDown = useCallback((e) => {
1071
+ const target = e.target;
1072
+ const cell = target.closest('td');
1073
+ if (cell && editorRef.current?.contains(cell)) {
1074
+ // 이미지나 이미지 컨테이너를 드래그하는 경우 셀 선택 방지
1075
+ if (target.tagName === 'IMG' || target.classList.contains('image-container')) {
1076
+ return;
1077
+ }
1078
+ // 마우스 다운 상태 설정
1079
+ isMouseDownRef.current = true;
1080
+ // 드래그 시작 셀 설정
1081
+ setSelectionStartCell(cell);
1082
+ // 이미 선택된 셀을 클릭한 경우 선택 유지
1083
+ const isAlreadySelected = cell.classList.contains('selected-cell');
1084
+ // 새로운 셀을 클릭하거나 Shift 키를 누르지 않은 경우에만 기존 선택 해제
1085
+ if (!isAlreadySelected && !e.shiftKey) {
1086
+ const allCells = editorRef.current.querySelectorAll('.selected-cell');
1087
+ allCells.forEach(c => c.classList.remove('selected-cell'));
1088
+ setSelectedTableCells([]);
1089
+ }
1090
+ }
1091
+ }, []);
1092
+ // 표 셀 마우스 이동 (드래그 선택 중)
1093
+ const handleCellMouseMove = useCallback((e) => {
1094
+ const target = e.target;
1095
+ const cell = target.closest('td');
1096
+ if (!cell || !editorRef.current?.contains(cell))
1097
+ return;
1098
+ // 마우스가 눌려있지 않으면 드래그 불가
1099
+ if (!isMouseDownRef.current) {
1100
+ return;
1101
+ }
1102
+ // selectionStartCell이 있고, 다른 셀로 이동한 경우에만 드래그 선택 모드 활성화
1103
+ if (selectionStartCell && cell !== selectionStartCell && !isSelectingCellsRef.current) {
1104
+ isSelectingCellsRef.current = true;
1105
+ setIsSelectingCells(true);
1106
+ e.preventDefault();
1107
+ e.stopPropagation();
1108
+ }
1109
+ if (!isSelectingCellsRef.current || !selectionStartCell)
1110
+ return;
1111
+ e.preventDefault();
1112
+ e.stopPropagation();
1113
+ // 범위 내 모든 셀 선택
1114
+ const cellsInRange = getCellsInRange(selectionStartCell, cell);
1115
+ // 기존 선택 클래스 제거
1116
+ const allSelectedCells = editorRef.current.querySelectorAll('.selected-cell');
1117
+ allSelectedCells.forEach(c => c.classList.remove('selected-cell'));
1118
+ // 새 선택 적용
1119
+ setSelectedTableCells(cellsInRange);
1120
+ cellsInRange.forEach(c => c.classList.add('selected-cell'));
1121
+ }, [selectionStartCell]);
1122
+ // 표 셀 마우스 업 (드래그 선택 종료)
1123
+ const handleCellMouseUp = useCallback((e) => {
1124
+ const target = e.target;
1125
+ const cell = target.closest('td');
1126
+ // 드래그 선택 중이었다면 플래그 설정
1127
+ if (isSelectingCellsRef.current) {
1128
+ // 셀 내부에서 마우스 업한 경우 이벤트 방지
1129
+ if (cell && editorRef.current?.contains(cell)) {
1130
+ e.preventDefault();
1131
+ e.stopPropagation();
1132
+ }
1133
+ // 드래그가 방금 끝났음을 표시
1134
+ justFinishedDraggingRef.current = true;
1135
+ // 50ms 후 플래그 해제 (클릭 이벤트가 처리된 후)
1136
+ setTimeout(() => {
1137
+ justFinishedDraggingRef.current = false;
1138
+ }, 50);
1139
+ }
1140
+ // 마우스 다운 상태 해제 (가장 중요!)
1141
+ isMouseDownRef.current = false;
1142
+ // 드래그 선택 모드 무조건 종료 (선택된 셀은 유지)
1143
+ isSelectingCellsRef.current = false;
1144
+ setIsSelectingCells(false);
1145
+ // selectionStartCell은 유지하여 선택 상태 보존
1146
+ }, []);
1147
+ // 셀 선택 해제
1148
+ const clearCellSelection = () => {
1149
+ selectedTableCells.forEach(cell => cell.classList.remove('selected-cell'));
1150
+ setSelectedTableCells([]);
1151
+ setSelectionStartCell(null);
1152
+ };
1153
+ // 표 셀 우클릭 이벤트 처리
1154
+ const handleEditorContextMenu = (e) => {
1155
+ const target = e.target;
1156
+ // 표 셀 우클릭 감지 (td 또는 td 내부 요소)
1157
+ const cell = target.closest('td');
1158
+ if (cell && editorRef.current?.contains(cell)) {
1159
+ e.preventDefault();
1160
+ e.stopPropagation();
1161
+ // 선택된 셀이 없거나, 우클릭한 셀이 선택 영역에 포함되지 않은 경우
1162
+ if (selectedTableCells.length === 0 || !selectedTableCells.includes(cell)) {
1163
+ clearCellSelection();
1164
+ setSelectedTableCell(cell);
1165
+ }
1166
+ else {
1167
+ // 선택된 셀들 중 하나를 우클릭한 경우, 첫 번째 셀을 대표로 사용
1168
+ setSelectedTableCell(selectedTableCells[0]);
1169
+ }
1170
+ setTableContextMenuPosition({ x: e.clientX, y: e.clientY });
1171
+ setIsTableContextMenuOpen(true);
1172
+ setIsTableCellColorOpen(false);
1173
+ }
1174
+ };
932
1175
  // 링크 수정
933
1176
  const updateLink = () => {
934
1177
  if (selectedLinkElement && editLinkUrl) {
@@ -1007,8 +1250,7 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
1007
1250
  });
1008
1251
  imageSrc = imageUrl;
1009
1252
  }
1010
- catch (error) {
1011
- console.error('Image validation failed:', error);
1253
+ catch {
1012
1254
  alert(`이미지를 불러올 수 없습니다.\n\n가능한 원인:\n1. 잘못된 이미지 URL\n2. CORS 정책으로 인한 차단 (외부 도메인)\n3. 네트워크 연결 문제\n4. 이미지가 존재하지 않음\n\nURL: ${imageUrl}\n\n💡 팁: CORS 정책으로 차단된 경우, 이미지를 직접 다운로드 후 파일 업로드를 사용해주세요.`);
1013
1255
  return;
1014
1256
  }
@@ -1024,7 +1266,6 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
1024
1266
  img.style.verticalAlign = 'middle'; // 수직 정렬 개선
1025
1267
  // 이미지 로드 에러 처리
1026
1268
  img.onerror = () => {
1027
- console.error('Image load failed:', imageSrc);
1028
1269
  alert(`이미지를 불러올 수 없습니다.\n\n가능한 원인:\n1. 잘못된 이미지 URL\n2. CORS 정책으로 인한 차단\n3. 네트워크 연결 문제\n\nURL: ${imageSrc}`);
1029
1270
  // 에러 발생 시 삽입된 이미지 제거
1030
1271
  if (img.parentNode) {
@@ -1401,6 +1642,274 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
1401
1642
  handleInput();
1402
1643
  };
1403
1644
  // YouTube 삽입
1645
+ // 표 삽입 함수
1646
+ const insertTable = (rows, cols) => {
1647
+ if (rows === 0 || cols === 0)
1648
+ return;
1649
+ // 표 HTML 생성
1650
+ const table = document.createElement('table');
1651
+ table.style.borderCollapse = 'collapse';
1652
+ table.style.width = '100%';
1653
+ table.style.margin = '10px 0';
1654
+ table.setAttribute('border', '1');
1655
+ table.style.border = '1px solid #ddd';
1656
+ const tbody = document.createElement('tbody');
1657
+ for (let i = 0; i < rows; i++) {
1658
+ const tr = document.createElement('tr');
1659
+ for (let j = 0; j < cols; j++) {
1660
+ const td = document.createElement('td');
1661
+ td.style.border = '1px solid #ddd';
1662
+ td.style.padding = '8px';
1663
+ td.style.minWidth = '50px';
1664
+ td.innerHTML = '<br>';
1665
+ tr.appendChild(td);
1666
+ }
1667
+ tbody.appendChild(tr);
1668
+ }
1669
+ table.appendChild(tbody);
1670
+ // 에디터에 포커스 설정
1671
+ if (editorRef.current) {
1672
+ editorRef.current.focus();
1673
+ const selection = window.getSelection();
1674
+ // 저장된 선택 영역이 있으면 복원
1675
+ if (savedTableSelection && selection) {
1676
+ try {
1677
+ selection.removeAllRanges();
1678
+ selection.addRange(savedTableSelection);
1679
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1680
+ }
1681
+ catch (e) {
1682
+ // 선택 영역 복원 실패 시 무시
1683
+ }
1684
+ }
1685
+ // 선택 영역 재확인
1686
+ if (!selection || selection.rangeCount === 0 || !editorRef.current.contains(selection.anchorNode)) {
1687
+ // 에디터가 비어있으면 p 태그 추가
1688
+ if (!editorRef.current.innerHTML || editorRef.current.innerHTML === '<br>') {
1689
+ const p = document.createElement('p');
1690
+ p.innerHTML = '<br>';
1691
+ editorRef.current.appendChild(p);
1692
+ }
1693
+ // 커서를 에디터 끝으로 이동
1694
+ const range = document.createRange();
1695
+ range.selectNodeContents(editorRef.current);
1696
+ range.collapse(false);
1697
+ selection?.removeAllRanges();
1698
+ selection?.addRange(range);
1699
+ }
1700
+ // 표 삽입
1701
+ if (selection && selection.rangeCount > 0) {
1702
+ const range = selection.getRangeAt(0);
1703
+ range.deleteContents();
1704
+ range.insertNode(table);
1705
+ // 표 다음에 새 문단 추가
1706
+ const newP = document.createElement('p');
1707
+ newP.innerHTML = '<br>';
1708
+ table.after(newP);
1709
+ // 커서를 첫 번째 셀로 이동
1710
+ const firstCell = table.querySelector('td');
1711
+ if (firstCell) {
1712
+ const newRange = document.createRange();
1713
+ newRange.selectNodeContents(firstCell);
1714
+ newRange.collapse(true);
1715
+ selection.removeAllRanges();
1716
+ selection.addRange(newRange);
1717
+ }
1718
+ }
1719
+ else {
1720
+ // 폴백: 에디터 끝에 추가
1721
+ editorRef.current.appendChild(table);
1722
+ }
1723
+ }
1724
+ // 상태 초기화
1725
+ setIsTableDropdownOpen(false);
1726
+ setTableRows(0);
1727
+ setTableCols(0);
1728
+ setSavedTableSelection(null);
1729
+ editorRef.current?.focus();
1730
+ handleInput();
1731
+ };
1732
+ // 다중 셀 선택 범위 계산
1733
+ const getCellsInRange = (startCell, endCell) => {
1734
+ const table = startCell.closest('table');
1735
+ if (!table)
1736
+ return [];
1737
+ const tbody = table.querySelector('tbody');
1738
+ if (!tbody)
1739
+ return [];
1740
+ const startRow = startCell.parentElement;
1741
+ const endRow = endCell.parentElement;
1742
+ const startRowIndex = Array.from(tbody.rows).indexOf(startRow);
1743
+ const endRowIndex = Array.from(tbody.rows).indexOf(endRow);
1744
+ const startColIndex = startCell.cellIndex;
1745
+ const endColIndex = endCell.cellIndex;
1746
+ const minRow = Math.min(startRowIndex, endRowIndex);
1747
+ const maxRow = Math.max(startRowIndex, endRowIndex);
1748
+ const minCol = Math.min(startColIndex, endColIndex);
1749
+ const maxCol = Math.max(startColIndex, endColIndex);
1750
+ const cells = [];
1751
+ for (let r = minRow; r <= maxRow; r++) {
1752
+ const row = tbody.rows[r];
1753
+ for (let c = minCol; c <= maxCol; c++) {
1754
+ if (row.cells[c]) {
1755
+ cells.push(row.cells[c]);
1756
+ }
1757
+ }
1758
+ }
1759
+ return cells;
1760
+ };
1761
+ // 표 셀 배경색 변경 (단일/다중)
1762
+ const changeTableCellBackgroundColor = (color) => {
1763
+ // 다중 셀이 선택되어 있으면 모든 선택된 셀에 적용
1764
+ if (selectedTableCells.length > 0) {
1765
+ selectedTableCells.forEach(cell => {
1766
+ cell.style.backgroundColor = color;
1767
+ });
1768
+ }
1769
+ else if (selectedTableCell) {
1770
+ // 단일 셀에만 적용
1771
+ selectedTableCell.style.backgroundColor = color;
1772
+ }
1773
+ setIsTableCellColorOpen(false);
1774
+ handleInput();
1775
+ };
1776
+ // 셀 배경색 초기화
1777
+ const resetTableCellBackgroundColor = () => {
1778
+ if (selectedTableCells.length > 0) {
1779
+ selectedTableCells.forEach(cell => {
1780
+ cell.style.backgroundColor = '';
1781
+ });
1782
+ }
1783
+ else if (selectedTableCell) {
1784
+ selectedTableCell.style.backgroundColor = '';
1785
+ }
1786
+ setIsTableCellColorOpen(false);
1787
+ handleInput();
1788
+ };
1789
+ // 셀 정렬 설정
1790
+ const changeTableCellAlign = (align) => {
1791
+ if (selectedTableCells.length > 0) {
1792
+ selectedTableCells.forEach(cell => {
1793
+ cell.style.textAlign = align;
1794
+ });
1795
+ }
1796
+ else if (selectedTableCell) {
1797
+ selectedTableCell.style.textAlign = align;
1798
+ }
1799
+ handleInput();
1800
+ };
1801
+ // 행 추가 (위/아래)
1802
+ const addTableRow = (position) => {
1803
+ if (!selectedTableCell)
1804
+ return;
1805
+ const row = selectedTableCell.closest('tr');
1806
+ if (!row)
1807
+ return;
1808
+ const table = row.closest('table');
1809
+ if (!table)
1810
+ return;
1811
+ const newRow = document.createElement('tr');
1812
+ const cellCount = row.cells.length;
1813
+ for (let i = 0; i < cellCount; i++) {
1814
+ const td = document.createElement('td');
1815
+ td.style.border = '1px solid #ddd';
1816
+ td.style.padding = '8px';
1817
+ td.style.minWidth = '50px';
1818
+ td.innerHTML = '<br>';
1819
+ newRow.appendChild(td);
1820
+ }
1821
+ if (position === 'above') {
1822
+ row.parentNode?.insertBefore(newRow, row);
1823
+ }
1824
+ else {
1825
+ row.parentNode?.insertBefore(newRow, row.nextSibling);
1826
+ }
1827
+ setIsTableContextMenuOpen(false);
1828
+ handleInput();
1829
+ };
1830
+ // 행 삭제
1831
+ const deleteTableRow = () => {
1832
+ if (!selectedTableCell)
1833
+ return;
1834
+ const row = selectedTableCell.closest('tr');
1835
+ if (!row)
1836
+ return;
1837
+ const tbody = row.parentNode;
1838
+ if (!tbody)
1839
+ return;
1840
+ // 마지막 행이면 삭제 불가
1841
+ if (tbody.rows.length <= 1) {
1842
+ alert('표에는 최소 1개의 행이 필요합니다.');
1843
+ return;
1844
+ }
1845
+ row.remove();
1846
+ setIsTableContextMenuOpen(false);
1847
+ setSelectedTableCell(null);
1848
+ handleInput();
1849
+ };
1850
+ // 열 추가 (좌/우)
1851
+ const addTableColumn = (position) => {
1852
+ if (!selectedTableCell)
1853
+ return;
1854
+ const cellIndex = selectedTableCell.cellIndex;
1855
+ const row = selectedTableCell.closest('tr');
1856
+ if (!row)
1857
+ return;
1858
+ const table = row.closest('table');
1859
+ if (!table)
1860
+ return;
1861
+ const tbody = table.querySelector('tbody');
1862
+ if (!tbody)
1863
+ return;
1864
+ Array.from(tbody.rows).forEach(row => {
1865
+ const newCell = document.createElement('td');
1866
+ newCell.style.border = '1px solid #ddd';
1867
+ newCell.style.padding = '8px';
1868
+ newCell.style.minWidth = '50px';
1869
+ newCell.innerHTML = '<br>';
1870
+ if (position === 'left') {
1871
+ row.insertBefore(newCell, row.cells[cellIndex]);
1872
+ }
1873
+ else {
1874
+ if (cellIndex + 1 < row.cells.length) {
1875
+ row.insertBefore(newCell, row.cells[cellIndex + 1]);
1876
+ }
1877
+ else {
1878
+ row.appendChild(newCell);
1879
+ }
1880
+ }
1881
+ });
1882
+ setIsTableContextMenuOpen(false);
1883
+ handleInput();
1884
+ };
1885
+ // 열 삭제
1886
+ const deleteTableColumn = () => {
1887
+ if (!selectedTableCell)
1888
+ return;
1889
+ const cellIndex = selectedTableCell.cellIndex;
1890
+ const row = selectedTableCell.closest('tr');
1891
+ if (!row)
1892
+ return;
1893
+ const table = row.closest('table');
1894
+ if (!table)
1895
+ return;
1896
+ const tbody = table.querySelector('tbody');
1897
+ if (!tbody)
1898
+ return;
1899
+ // 마지막 열이면 삭제 불가
1900
+ if (row.cells.length <= 1) {
1901
+ alert('표에는 최소 1개의 열이 필요합니다.');
1902
+ return;
1903
+ }
1904
+ Array.from(tbody.rows).forEach(row => {
1905
+ if (row.cells[cellIndex]) {
1906
+ row.cells[cellIndex].remove();
1907
+ }
1908
+ });
1909
+ setIsTableContextMenuOpen(false);
1910
+ setSelectedTableCell(null);
1911
+ handleInput();
1912
+ };
1404
1913
  const insertYoutube = () => {
1405
1914
  if (!youtubeUrl)
1406
1915
  return;
@@ -1645,11 +2154,116 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
1645
2154
  return;
1646
2155
  }
1647
2156
  const range = selection.getRangeAt(0);
1648
- // 선택된 텍스트를 span으로 감싸기
2157
+ // 선택 영역에 포함된 모든 표 셀 찾기
2158
+ const getSelectedTableCells = () => {
2159
+ const cells = [];
2160
+ const container = range.commonAncestorContainer;
2161
+ // 컨테이너가 표인지 확인
2162
+ let tableElement = null;
2163
+ let current = container.nodeType === Node.TEXT_NODE ? container.parentElement : container;
2164
+ while (current && current !== editorRef.current) {
2165
+ if (current.tagName === 'TABLE' || current.tagName === 'TBODY' || current.tagName === 'TR') {
2166
+ // 상위 table 요소 찾기
2167
+ let table = current;
2168
+ while (table && table.tagName !== 'TABLE') {
2169
+ table = table.parentElement;
2170
+ }
2171
+ tableElement = table;
2172
+ break;
2173
+ }
2174
+ current = current.parentElement;
2175
+ }
2176
+ if (!tableElement)
2177
+ return cells;
2178
+ // 표 내의 모든 셀 확인
2179
+ const allCells = tableElement.querySelectorAll('td, th');
2180
+ allCells.forEach(cell => {
2181
+ if (range.intersectsNode(cell)) {
2182
+ cells.push(cell);
2183
+ }
2184
+ });
2185
+ return cells;
2186
+ };
2187
+ const selectedCells = getSelectedTableCells();
2188
+ // 여러 표 셀이 선택된 경우
2189
+ if (selectedCells.length > 1) {
2190
+ selectedCells.forEach(cell => {
2191
+ // 각 셀의 모든 내용을 span으로 감싸기
2192
+ const cellContents = Array.from(cell.childNodes);
2193
+ cellContents.forEach(node => {
2194
+ if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim()) {
2195
+ // 텍스트 노드를 span으로 감싸기
2196
+ const span = document.createElement('span');
2197
+ if (styleProperty === 'color') {
2198
+ span.style.color = color;
2199
+ }
2200
+ else if (styleProperty === 'background-color') {
2201
+ span.style.backgroundColor = color;
2202
+ }
2203
+ span.textContent = node.textContent;
2204
+ cell.replaceChild(span, node);
2205
+ }
2206
+ else if (node.nodeType === Node.ELEMENT_NODE) {
2207
+ // 기존 요소에 스타일 적용
2208
+ const element = node;
2209
+ if (styleProperty === 'color') {
2210
+ element.style.color = color;
2211
+ }
2212
+ else if (styleProperty === 'background-color') {
2213
+ element.style.backgroundColor = color;
2214
+ }
2215
+ }
2216
+ });
2217
+ });
2218
+ // 선택 해제
2219
+ selection.removeAllRanges();
2220
+ editorRef.current?.focus();
2221
+ handleInput();
2222
+ return;
2223
+ }
2224
+ // 단일 셀 내부 또는 일반 텍스트
2225
+ const commonAncestor = range.commonAncestorContainer;
2226
+ // 선택 영역이 표 셀 내부인지 확인
2227
+ const isInTableCell = (node) => {
2228
+ let current = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
2229
+ while (current && current !== editorRef.current) {
2230
+ if (current.tagName === 'TD' || current.tagName === 'TH') {
2231
+ return true;
2232
+ }
2233
+ current = current.parentElement;
2234
+ }
2235
+ return false;
2236
+ };
2237
+ // 표 셀 내부에서의 색상 변경 (단일 셀)
2238
+ if (isInTableCell(commonAncestor)) {
2239
+ try {
2240
+ const contents = range.extractContents();
2241
+ const span = document.createElement('span');
2242
+ if (styleProperty === 'color') {
2243
+ span.style.color = color;
2244
+ }
2245
+ else if (styleProperty === 'background-color') {
2246
+ span.style.backgroundColor = color;
2247
+ }
2248
+ span.appendChild(contents);
2249
+ range.insertNode(span);
2250
+ // 커서 위치 조정
2251
+ range.setStartAfter(span);
2252
+ range.collapse(true);
2253
+ selection.removeAllRanges();
2254
+ selection.addRange(range);
2255
+ editorRef.current?.focus();
2256
+ handleInput();
2257
+ return;
2258
+ }
2259
+ catch {
2260
+ // 오류 무시
2261
+ }
2262
+ }
2263
+ // 일반 텍스트에 대한 색상 변경
1649
2264
  const span = document.createElement('span');
1650
2265
  try {
1651
2266
  const contents = range.extractContents();
1652
- // 스타일 적용 - setAttribute를 사용하여 !important 포함
1653
2267
  if (styleProperty === 'color') {
1654
2268
  span.setAttribute('style', `color: ${color} !important;`);
1655
2269
  }
@@ -1658,14 +2272,12 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
1658
2272
  }
1659
2273
  span.appendChild(contents);
1660
2274
  range.insertNode(span);
1661
- // 커서 위치 조정
1662
2275
  range.selectNodeContents(span);
1663
2276
  range.collapse(false);
1664
2277
  selection.removeAllRanges();
1665
2278
  selection.addRange(range);
1666
2279
  }
1667
2280
  catch {
1668
- // 폴백: execCommand 사용
1669
2281
  if (styleProperty === 'color') {
1670
2282
  document.execCommand('foreColor', false, color);
1671
2283
  }
@@ -1737,6 +2349,16 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
1737
2349
  setYoutubeUrl('');
1738
2350
  setSavedYoutubeSelection(null);
1739
2351
  }
2352
+ // 표 드롭다운 체크
2353
+ const tableDropdown = document.querySelector(`.${styles.tableDropdown}`);
2354
+ if (tableButtonRef.current &&
2355
+ !tableButtonRef.current.contains(target) &&
2356
+ (!tableDropdown || !tableDropdown.contains(target))) {
2357
+ setIsTableDropdownOpen(false);
2358
+ setTableRows(0);
2359
+ setTableCols(0);
2360
+ setSavedTableSelection(null);
2361
+ }
1740
2362
  // 이미지 편집 팝업 닫기
1741
2363
  if (isImageEditPopupOpen && selectedImage) {
1742
2364
  const imageEditPopup = document.querySelector(`.${styles.imageDropdown}`);
@@ -1758,14 +2380,20 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
1758
2380
  setEditLinkTarget('_self');
1759
2381
  }
1760
2382
  }
2383
+ // 표 컨텍스트 메뉴 닫기
2384
+ if (isTableContextMenuOpen && tableContextMenuRef.current && !tableContextMenuRef.current.contains(target)) {
2385
+ setIsTableContextMenuOpen(false);
2386
+ setSelectedTableCell(null);
2387
+ setIsTableCellColorOpen(false);
2388
+ }
1761
2389
  };
1762
- if (isParagraphDropdownOpen || isTextColorOpen || isBgColorOpen || isAlignDropdownOpen || isLinkDropdownOpen || isEditLinkPopupOpen || isImageDropdownOpen || isImageEditPopupOpen || isYoutubeDropdownOpen) {
2390
+ if (isParagraphDropdownOpen || isTextColorOpen || isBgColorOpen || isAlignDropdownOpen || isLinkDropdownOpen || isEditLinkPopupOpen || isImageDropdownOpen || isImageEditPopupOpen || isYoutubeDropdownOpen || isTableDropdownOpen || isTableContextMenuOpen) {
1763
2391
  document.addEventListener('mousedown', handleClickOutside);
1764
2392
  }
1765
2393
  return () => {
1766
2394
  document.removeEventListener('mousedown', handleClickOutside);
1767
2395
  };
1768
- }, [isParagraphDropdownOpen, isTextColorOpen, isBgColorOpen, isAlignDropdownOpen, isLinkDropdownOpen, isEditLinkPopupOpen, isImageDropdownOpen, isImageEditPopupOpen, isYoutubeDropdownOpen, selectedLinkElement, selectedImage]);
2396
+ }, [isParagraphDropdownOpen, isTextColorOpen, isBgColorOpen, isAlignDropdownOpen, isLinkDropdownOpen, isEditLinkPopupOpen, isImageDropdownOpen, isImageEditPopupOpen, isYoutubeDropdownOpen, isTableDropdownOpen, isTableContextMenuOpen, selectedLinkElement, selectedImage]);
1769
2397
  // 리사이즈 중 마우스 이벤트 처리
1770
2398
  useEffect(() => {
1771
2399
  if (!isResizing || !resizeStartData)
@@ -1890,6 +2518,20 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
1890
2518
  document.removeEventListener('mouseup', handleMouseUp);
1891
2519
  };
1892
2520
  }, [isResizing, resizeStartData, selectedImage, selectedYoutube]);
2521
+ // 표 셀 드래그 선택 이벤트 등록
2522
+ useEffect(() => {
2523
+ if (!editorRef.current || isCodeView)
2524
+ return;
2525
+ const editor = editorRef.current;
2526
+ editor.addEventListener('mousedown', handleCellMouseDown);
2527
+ document.addEventListener('mousemove', handleCellMouseMove);
2528
+ document.addEventListener('mouseup', handleCellMouseUp);
2529
+ return () => {
2530
+ editor.removeEventListener('mousedown', handleCellMouseDown);
2531
+ document.removeEventListener('mousemove', handleCellMouseMove);
2532
+ document.removeEventListener('mouseup', handleCellMouseUp);
2533
+ };
2534
+ }, [handleCellMouseDown, handleCellMouseMove, handleCellMouseUp, isCodeView]);
1893
2535
  // 스크롤, 리사이즈 및 이미지/유튜브 드래그 시 편집창 숨기기
1894
2536
  useEffect(() => {
1895
2537
  if (!selectedImage && !selectedYoutube)
@@ -2066,7 +2708,7 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
2066
2708
  }, 100);
2067
2709
  return () => clearTimeout(timer);
2068
2710
  }, []);
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: {
2711
+ 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
2712
  opacity: historyIndex <= 0 ? 0.65 : 1,
2071
2713
  backgroundColor: 'transparent',
2072
2714
  border: 'none',
@@ -2076,7 +2718,7 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
2076
2718
  backgroundColor: 'transparent',
2077
2719
  border: 'none',
2078
2720
  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: () => {
2721
+ }, 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
2722
  setIsParagraphDropdownOpen(!isParagraphDropdownOpen);
2081
2723
  setIsTextColorOpen(false);
2082
2724
  setIsBgColorOpen(false);
@@ -2084,7 +2726,7 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
2084
2726
  }, title: "\uBB38\uB2E8 \uD615\uC2DD", children: [_jsx("span", { children: getCurrentStyleLabel() }), _jsx("i", { className: styles.dropdownArrow })] }), isParagraphDropdownOpen && (_jsx("div", { className: styles.paragraphDropdown, style: {
2085
2727
  top: paragraphButtonRef.current?.getBoundingClientRect().bottom ?? 0,
2086
2728
  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: () => {
2729
+ }, 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
2730
  const selection = window.getSelection();
2089
2731
  if (selection && !selection.isCollapsed) {
2090
2732
  // 선택 영역 저장
@@ -2116,7 +2758,7 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
2116
2758
  changeBackgroundColor(color, savedSelection);
2117
2759
  setIsBgColorOpen(false);
2118
2760
  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: () => {
2761
+ } }, 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
2762
  setIsAlignDropdownOpen(!isAlignDropdownOpen);
2121
2763
  setIsParagraphDropdownOpen(false);
2122
2764
  setIsTextColorOpen(false);
@@ -2136,7 +2778,22 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
2136
2778
  }
2137
2779
  setCurrentAlign(option.value);
2138
2780
  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 }) })] }), _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: {
2781
+ }, 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: () => {
2782
+ // 현재 선택 영역 저장
2783
+ const selection = window.getSelection();
2784
+ if (selection && selection.rangeCount > 0) {
2785
+ setSavedTableSelection(selection.getRangeAt(0).cloneRange());
2786
+ }
2787
+ setIsTableDropdownOpen(!isTableDropdownOpen);
2788
+ }, title: "\uD45C \uC0BD\uC785", children: _jsx("i", { className: styles.table }) }), isTableDropdownOpen && (_jsx("div", { className: styles.tableDropdown, style: {
2789
+ top: tableButtonRef.current?.getBoundingClientRect().bottom ?? 0,
2790
+ left: tableButtonRef.current?.getBoundingClientRect().left ?? 0
2791
+ }, 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: () => {
2792
+ setTableRows(row);
2793
+ setTableCols(col);
2794
+ }, onClick: () => {
2795
+ insertTable(row, col);
2796
+ } }, `${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
2797
  top: linkButtonRef.current?.getBoundingClientRect().bottom ?? 0,
2141
2798
  left: linkButtonRef.current?.getBoundingClientRect().left ?? 0
2142
2799
  }, 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 +2801,7 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
2144
2801
  setLinkUrl('');
2145
2802
  setLinkTarget('_blank');
2146
2803
  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: {
2804
+ }, 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
2805
  top: imageButtonRef.current?.getBoundingClientRect().bottom ?? 0,
2149
2806
  left: imageButtonRef.current?.getBoundingClientRect().left ?? 0
2150
2807
  }, 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 +2823,7 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
2166
2823
  setImageAlign('left'); // 좌측으로 초기화
2167
2824
  setImageAlt('');
2168
2825
  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) => {
2826
+ }, 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
2827
  e.stopPropagation();
2171
2828
  // 현재 선택 영역 저장
2172
2829
  const selection = window.getSelection();
@@ -2193,7 +2850,7 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
2193
2850
  setYoutubeWidth('100%');
2194
2851
  setYoutubeAlign('center');
2195
2852
  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: {
2853
+ }, 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
2854
  height: height === 'contents' ? 'auto' : (height || '300px'),
2198
2855
  minHeight: minHeight || (height === 'contents' ? '100px' : '200px'),
2199
2856
  maxHeight: maxHeight || (height === 'contents' ? undefined : undefined),
@@ -2206,7 +2863,7 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
2206
2863
  minHeight: height === 'contents' ? 'auto' : 0,
2207
2864
  height: height === 'contents' && savedEditorHeight ? `${savedEditorHeight}px` : undefined,
2208
2865
  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: () => {
2866
+ }, 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
2867
  detectCurrentParagraphStyle();
2211
2868
  detectCurrentAlign();
2212
2869
  }, onKeyDown: handleKeyDown, style: {
@@ -2248,6 +2905,11 @@ const Editor = ({ value = '', width = '100%', height = '400px', minHeight, maxHe
2248
2905
  minWidth: '360px',
2249
2906
  maxWidth: '90%'
2250
2907
  }, 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
- })()] }));
2908
+ })(), isTableContextMenuOpen && selectedTableCell && (_jsxs("div", { ref: tableContextMenuRef, className: styles.tableContextMenu, style: {
2909
+ position: 'fixed',
2910
+ top: tableContextMenuPosition.y,
2911
+ left: tableContextMenuPosition.x,
2912
+ zIndex: 10000
2913
+ }, 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
2914
  };
2253
2915
  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.14",
3
+ "version": "0.3.16",
4
4
  "type": "module",
5
5
  "author": "hada0127 <work@tarucy.net>",
6
6
  "license": "MIT",
@@ -80,6 +80,7 @@
80
80
  "devDependencies": {
81
81
  "@cloudflare/next-on-pages": "^1.13.8",
82
82
  "@eslint/js": "^9.8.0",
83
+ "@playwright/test": "^1.56.1",
83
84
  "@types/node": "22.10.2",
84
85
  "@types/react": "^18.3.3",
85
86
  "@types/react-dom": "^18.3.0",