diff --git a/frontend/src/assets/toolbox-colon-icon.svg b/frontend/src/assets/toolbox-colon-icon.svg new file mode 100644 index 00000000..49f634ee --- /dev/null +++ b/frontend/src/assets/toolbox-colon-icon.svg @@ -0,0 +1,6 @@ + diff --git a/frontend/src/assets/toolbox-line-icon.svg b/frontend/src/assets/toolbox-line-icon.svg new file mode 100644 index 00000000..b3f9debe --- /dev/null +++ b/frontend/src/assets/toolbox-line-icon.svg @@ -0,0 +1,3 @@ + diff --git a/frontend/src/components/Board/RichTextEditor.jsx b/frontend/src/components/Board/RichTextEditor.jsx index 64b1b9ff..08b93011 100644 --- a/frontend/src/components/Board/RichTextEditor.jsx +++ b/frontend/src/components/Board/RichTextEditor.jsx @@ -17,6 +17,8 @@ import toolboxFileIcon from '../../assets/toolbox-file-icon.svg'; import toolboxLeftIcon from '../../assets/toolbox-left-icon.svg'; import toolboxMidIcon from '../../assets/toolbox-mid-icon.svg'; import toolboxRightIcon from '../../assets/toolbox-right-icon.svg'; +import toolboxLineIcon from '../../assets/toolbox-line-icon.svg'; +import toolboxColonIcon from '../../assets/toolbox-colon-icon.svg'; import toolboxTextColorIcon from '../../assets/toolbox-textcolor-icon.svg'; import toolboxTextBgIcon from '../../assets/toolbox-textBackgroundColor-icon.svg'; @@ -41,17 +43,8 @@ const hasClipboardImage = async () => { } }; -const FONT_FAMILY_OPTIONS = [ - { label: '기본', value: '' }, - { label: 'Pretendard', value: 'Pretendard' }, - { label: 'Noto Sans KR', value: 'Noto Sans KR' }, - { label: 'Apple SD Gothic Neo', value: 'Apple SD Gothic Neo' }, - { label: 'Arial', value: 'Arial' }, - { label: 'Georgia', value: 'Georgia' }, - { label: 'Courier New', value: 'Courier New' }, -]; - -const FONT_SIZE_OPTIONS = [12, 14, 16, 18, 20, 24, 28, 32, 40]; +const DEFAULT_FONT_SIZE = 15; +const FONT_SIZE_OPTIONS = [12, 14, 15, 16, 18, 20, 24, 28, 32, 40]; const FontFamily = Extension.create({ name: 'fontFamily', @@ -179,33 +172,6 @@ const ResizableImageNodeView = (props) => { }; const onUp = () => { - // final sync: ensure attributes persisted in the ProseMirror document - try { - const imageEl = imageRef.current; - if (imageEl) { - const rectFinal = imageEl.getBoundingClientRect(); - const finalWidth = Math.max(MIN_IMAGE_WIDTH, Math.round(rectFinal.width)); - const finalHeight = Math.max(MIN_IMAGE_HEIGHT, Math.round(rectFinal.height)); - const attrs = {}; - if (isHorizontalOnly) attrs.width = `${finalWidth}px`; - else if (isVerticalOnly) attrs.height = `${finalHeight}px`; - else attrs.width = `${finalWidth}px`, attrs.height = `${finalHeight}px`; - - // update via provided updateAttributes (NodeView) and editor command to ensure persistence - try { - updateAttributes?.(attrs); - } catch {} - - try { - if (editor && editor.chain) { - editor.chain().focus().updateAttributes('image', attrs).run(); - } - } catch {} - } - } catch (e) { - // ignore - } - stopResizing(); }; @@ -587,7 +553,7 @@ const RichTextEditor = ({ // We'll handle both images and other files by inserting them into the editor content. // Prevent the default only when we actually handle insertion here. - let willHandle = imageFiles.length > 0 || otherFiles.length > 0; + const willHandle = imageFiles.length > 0 || otherFiles.length > 0; if (!willHandle) return false; event.preventDefault(); @@ -725,8 +691,8 @@ const RichTextEditor = ({ insertFileLink(normalizedUrl || '', uploaded?.originalFilename || file.name); } } - } catch (e) { - console.error('파일 업로드 실패:', e); + } catch (error) { + console.error('파일 업로드 실패:', error); } finally { event.target.value = ''; } @@ -754,37 +720,13 @@ const RichTextEditor = ({ insertFileLink(normalizedUrl || '', uploaded?.originalFilename || file.name); } } - } catch (e) { - console.error('비디오 업로드 실패:', e); + } catch (error) { + console.error('비디오 업로드 실패:', error); } finally { event.target.value = ''; } }; - const promptLink = () => { - if (!editor || !editable) return; - - const previousUrl = editor.getAttributes('link').href || ''; - const nextUrl = window.prompt('링크 주소를 입력하세요.', previousUrl); - - if (nextUrl === null) return; - - const trimmedUrl = nextUrl.trim(); - - if (!trimmedUrl) { - editor.chain().focus().extendMarkRange('link').unsetLink().run(); - return; - } - - editor.chain().focus().extendMarkRange('link').setLink({ href: trimmedUrl }).run(); - }; - - const clearFormatting = () => { - if (!editor || !editable) return; - - editor.chain().focus().unsetAllMarks().clearNodes().setParagraph().run(); - }; - const applyTextColor = (color) => { if (!editor || !editable) return; @@ -803,10 +745,13 @@ const RichTextEditor = ({ // prefer to unset Highlight, fallback to clearing textStyle background try { editor.chain().focus().unsetHighlight().run(); - } catch { + } catch (error) { + console.error('배경색 해제 실패:', error); try { editor.chain().focus().setMark('textStyle', { backgroundColor: null }).run(); - } catch {} + } catch (nestedError) { + console.error('배경색 해제 실패(textStyle):', nestedError); + } } return; } @@ -814,17 +759,26 @@ const RichTextEditor = ({ // prefer Highlight (supports multicolor), fallback to textStyle mark try { editor.chain().focus().toggleHighlight({ color }).run(); - } catch { + } catch (error) { + console.error('배경색 적용 실패:', error); try { editor.chain().focus().setMark('textStyle', { backgroundColor: color }).run(); - } catch {} + } catch (nestedError) { + console.error('배경색 적용 실패(textStyle):', nestedError); + } } }; - const applyFontFamily = (fontFamily) => { + const insertDividerLine = () => { if (!editor || !editable) return; - editor.chain().focus().setMark('textStyle', { fontFamily }).run(); + editor.chain().focus().setHorizontalRule().run(); + }; + + const insertQuoteBlock = () => { + if (!editor || !editable) return; + + editor.chain().focus().toggleBlockquote().run(); }; const applyFontSize = (fontSize) => { @@ -838,33 +792,7 @@ const RichTextEditor = ({ editor.chain().focus().setMark('textStyle', { fontSize: String(fontSize) }).run(); }; - const applyImageWidth = (width) => { - if (!editor || !editable || !editor.isActive('image')) return; - - const nextWidth = width ? String(width) : null; - editor.chain().focus().updateAttributes('image', { width: nextWidth }).run(); - }; - - const applyImageAlignment = (align) => { - if (!editor || !editable || !editor.isActive('image')) return; - - editor.chain().focus().updateAttributes('image', { align }).run(); - }; - - const resetSelectedImageSize = () => { - if (!editor || !editable || !editor.isActive('image')) return; - - editor.chain().focus().updateAttributes('image', { width: null }).run(); - }; - - const deleteSelectedImage = () => { - if (!editor || !editable || !editor.isActive('image')) return; - - editor.chain().focus().deleteSelection().run(); - }; - - const currentImageAlignment = editor?.isActive('image') ? editor.getAttributes('image')?.align || DEFAULT_IMAGE_ALIGN : DEFAULT_IMAGE_ALIGN; - const currentImageWidth = editor?.isActive('image') ? editor.getAttributes('image')?.width || '' : ''; + const currentFontSize = editor?.getAttributes('textStyle')?.fontSize || DEFAULT_FONT_SIZE; useEffect(() => { if (!editor) return; @@ -885,7 +813,9 @@ const RichTextEditor = ({ return () => { try { editor.off('selectionUpdate', onSelection); - } catch {} + } catch (error) { + console.error('selectionUpdate 해제 실패:', error); + } }; }, [editor]); @@ -909,8 +839,9 @@ const RichTextEditor = ({