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 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,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
- 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 => {
@@ -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
- // 선택된 텍스트를 span으로 감싸기
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 }) })] }), _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: {
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
+ }
@@ -1,4 +1,4 @@
1
- @use '../../mixin.scss' as *;
1
+ @use '../../../mixin.scss' as *;
2
2
  .style {
3
3
  display: flex;
4
4
  flex-direction: column;
@@ -1,4 +1,4 @@
1
- @use '../../mixin.scss' as *;
1
+ @use '../../../mixin.scss' as *;
2
2
  .style {
3
3
  display: flex;
4
4
  flex-direction: column;
@@ -1,4 +1,4 @@
1
- @use '../../mixin.scss' as *;
1
+ @use '../../../mixin.scss' as *;
2
2
  .style {
3
3
  width: 100%;
4
4
  display: flex;
@@ -1,4 +1,4 @@
1
- @use '../../mixin.scss' as *;
1
+ @use '../../../mixin.scss' as *;
2
2
 
3
3
  .pagination {
4
4
  display: flex;
@@ -1,4 +1,4 @@
1
- @use '../../mixin' as *;
1
+ @use '../../../mixin' as *;
2
2
 
3
3
  .toastPortal {
4
4
  position: fixed;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "podo-ui",
3
- "version": "0.3.13",
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": "npm run clean && tsc -p tsconfig.build.json && npm run copy:styles && npm run fix:scss-paths",
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",