From 75249e6fa46864f49d4eb91bd755171b6b65eaae Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Mon, 28 Jul 2025 12:10:39 +0000
Subject: (김준회) 공통모듈 - Knox 결재 모듈 구현, 유저 선택기 구현, 상신 결재
 저장을 위한 DB 스키마 및 서비스 추가, spreadjs 라이센스 환경변수 통일, 유저
 테이블에 epId 컬럼 추가
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 components/rich-text-editor/RichTextEditor.tsx | 998 +++++++++++++++++++++++++
 1 file changed, 998 insertions(+)
 create mode 100644 components/rich-text-editor/RichTextEditor.tsx

(limited to 'components/rich-text-editor/RichTextEditor.tsx')

diff --git a/components/rich-text-editor/RichTextEditor.tsx b/components/rich-text-editor/RichTextEditor.tsx
new file mode 100644
index 00000000..ceb76665
--- /dev/null
+++ b/components/rich-text-editor/RichTextEditor.tsx
@@ -0,0 +1,998 @@
+'use client'
+
+import React, { useCallback, useRef, useState, useEffect } from 'react'
+
+import { useEditor, EditorContent, type Editor } from '@tiptap/react'
+import StarterKit from '@tiptap/starter-kit'
+import Underline from '@tiptap/extension-underline'
+import { Image as TiptapImage } from '@tiptap/extension-image'
+import Link from '@tiptap/extension-link'
+import TextAlign from '@tiptap/extension-text-align'
+import TextStyle from '@tiptap/extension-text-style'
+import Subscript from '@tiptap/extension-subscript'
+import Superscript from '@tiptap/extension-superscript'
+import { Extension } from '@tiptap/core'
+import Highlight from '@tiptap/extension-highlight'
+import TaskList from '@tiptap/extension-task-list'
+import TaskItem from '@tiptap/extension-task-item'
+import BulletList from '@tiptap/extension-bullet-list'
+import ListItem from '@tiptap/extension-list-item'
+import OrderedList from '@tiptap/extension-ordered-list'
+import Blockquote from '@tiptap/extension-blockquote'
+import Table from '@tiptap/extension-table'
+import TableRow from '@tiptap/extension-table-row'
+import TableCell from '@tiptap/extension-table-cell'
+import TableHeader from '@tiptap/extension-table-header'
+
+// shadcn/ui & lucide
+import {
+  Bold,
+  Italic,
+  Underline as UnderlineIcon,
+  Strikethrough,
+  ListOrdered,
+  List,
+  Quote,
+  Undo,
+  Redo,
+  Link as LinkIcon,
+  Image as ImageIcon,
+  AlignLeft,
+  AlignCenter,
+  AlignRight,
+  AlignJustify,
+  Subscript as SubscriptIcon,
+  Superscript as SuperscriptIcon,
+  Table as TableIcon,
+  Highlighter,
+  CheckSquare,
+  Type,
+} from 'lucide-react'
+import { Toggle } from '@/components/ui/toggle'
+import { Separator } from '@/components/ui/separator'
+import { Input } from '@/components/ui/input'
+import { Button } from '@/components/ui/button'
+import { Label } from '@/components/ui/label'
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu'
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+  DialogTrigger,
+} from '@/components/ui/dialog'
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipProvider,
+  TooltipTrigger,
+} from '@/components/ui/tooltip'
+
+/* -------------------------------------------------------------------------------------------------
+ * FontSize extension (wraps TextStyle)
+ * -------------------------------------------------------------------------------------------------*/
+const FontSize = Extension.create({
+  name: 'fontSize',
+  addGlobalAttributes() {
+    return [
+      {
+        types: ['textStyle'],
+        attributes: {
+          fontSize: {
+            default: null,
+            parseHTML: element => {
+              const size = element.style.fontSize
+              return size ? size.replace(/[^0-9]/g, '') : null
+            },
+            renderHTML: attributes => {
+              if (!attributes.fontSize) return {}
+              return {
+                style: `font-size: ${attributes.fontSize}`,
+              }
+            },
+          },
+        },
+      },
+    ]
+  },
+})
+
+/* -------------------------------------------------------------------------------------------------
+ * Props & component
+ * -------------------------------------------------------------------------------------------------*/
+interface RichTextEditorProps {
+  value: string
+  onChange: (val: string) => void
+  disabled?: boolean
+  height?: string // e.g. "400px" or "100%"
+}
+
+export default function RichTextEditor({
+  value,
+  onChange,
+  disabled,
+  height = '300px',
+}: RichTextEditorProps) {
+  // ---------------------------------------------------------------------------
+  // Editor instance
+  // ---------------------------------------------------------------------------
+  const editor = useEditor({
+    extensions: [
+      StarterKit.configure({
+        bulletList: false,
+        orderedList: false,
+        listItem: false,
+        blockquote: false,
+        codeBlock: false,
+        code: false,
+        heading: { levels: [1, 2, 3] },
+        horizontalRule: false,
+      }),
+      Underline,
+      TiptapImage.configure({
+        HTMLAttributes: {
+          class: 'max-w-full h-auto',
+          style: 'max-width: 600px; height: auto;',
+        },
+      }),
+      Link.configure({ openOnClick: true, linkOnPaste: true }),
+      TextAlign.configure({
+        types: ['heading', 'paragraph'],
+        alignments: ['left', 'center', 'right', 'justify'],
+        defaultAlignment: 'left',
+      }),
+      Subscript,
+      Superscript,
+      TextStyle,
+      FontSize,
+      Table.configure({ resizable: true }),
+      TableRow,
+      TableCell,
+      TableHeader,
+      Highlight.configure({ multicolor: true }),
+      TaskList,
+      TaskItem.configure({ nested: true }),
+      BulletList,
+      ListItem,
+      OrderedList,
+      Blockquote,
+    ],
+    content: value,
+    editable: !disabled,
+    enablePasteRules: false,
+    enableInputRules: false,
+    immediatelyRender: false,
+    editorProps: {
+      attributes: {
+        class:
+          'w-full h-full min-h-full bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 prose prose-sm max-w-none',
+      },
+      handleDrop: (view, event, slice, moved) => {
+        if (!moved && event.dataTransfer?.files.length) {
+          const file = event.dataTransfer.files[0]
+          if (file.type.startsWith('image/')) {
+            handleImageUpload(file)
+            return true
+          }
+        }
+        return false
+      },
+      handlePaste: (view, event) => {
+        if (event.clipboardData?.files.length) {
+          const file = event.clipboardData.files[0]
+          if (file.type.startsWith('image/')) {
+            handleImageUpload(file)
+            return true
+          }
+        }
+        return false
+      },
+    },
+    onUpdate: ({ editor }) => {
+      onChange(editor.getHTML())
+    },
+  })
+
+  // ---------------------------------------------------------------------------
+  // Image handling (base64)
+  // ---------------------------------------------------------------------------
+  const handleImageUpload = async (file: File) => {
+    if (file.size > 3 * 1024 * 1024) {
+      alert('이미지 크기는 3 MB 이하만 지원됩니다.')
+      return
+    }
+    if (!file.type.startsWith('image/')) {
+      alert('이미지 파일만 업로드 가능합니다.')
+      return
+    }
+    const reader = new FileReader()
+    reader.onload = e => {
+      const base64 = e.target?.result as string
+      editor?.chain().focus().setImage({ src: base64, alt: file.name }).run()
+    }
+    reader.onerror = () => alert('이미지 읽기에 실패했습니다.')
+    reader.readAsDataURL(file)
+  }
+
+  // ---------------------------------------------------------------------------
+  // Toolbar (internal component)
+  // ---------------------------------------------------------------------------
+  const Toolbar: React.FC<{ editor: Editor | null; disabled?: boolean }> = ({
+    editor,
+    disabled,
+  }) => {
+    const [fontSize, setFontSize] = useState('16')
+    const [isTableDialogOpen, setIsTableDialogOpen] = useState(false)
+    const [tableRows, setTableRows] = useState('3')
+    const [tableCols, setTableCols] = useState('3')
+
+    // 간단한 툴바 상태 계산 - 실시간으로 계산하여 상태 동기화 문제 해결
+    const getToolbarState = useCallback(() => {
+      if (!editor) return {
+        bold: false,
+        italic: false,
+        underline: false,
+        strike: false,
+        bulletList: false,
+        orderedList: false,
+        blockquote: false,
+        link: false,
+        highlight: false,
+        taskList: false,
+        table: false,
+        subscript: false,
+        superscript: false,
+        heading: false,
+        textAlign: 'left' as 'left' | 'center' | 'right' | 'justify',
+      }
+
+      const textAlign = editor.isActive({ textAlign: 'center' })
+        ? 'center'
+        : editor.isActive({ textAlign: 'right' })
+          ? 'right'
+          : editor.isActive({ textAlign: 'justify' })
+            ? 'justify'
+            : 'left'
+
+      return {
+        bold: editor.isActive('bold'),
+        italic: editor.isActive('italic'),
+        underline: editor.isActive('underline'),
+        strike: editor.isActive('strike'),
+        bulletList: editor.isActive('bulletList'),
+        orderedList: editor.isActive('orderedList'),
+        blockquote: editor.isActive('blockquote'),
+        link: editor.isActive('link'),
+        highlight: editor.isActive('highlight'),
+        taskList: editor.isActive('taskList'),
+        table: editor.isActive('table'),
+        subscript: editor.isActive('subscript'),
+        superscript: editor.isActive('superscript'),
+        heading: [1, 2, 3, 4, 5, 6].some(l => editor.isActive('heading', { level: l })),
+        textAlign: textAlign as 'left' | 'center' | 'right' | 'justify',
+      }
+    }, [editor])
+
+    const toolbarState = getToolbarState()
+
+    // 폰트 사이즈 업데이트 - 복잡한 timeout 로직 제거
+    useEffect(() => {
+      if (!editor) return
+      
+      const updateFontSize = () => {
+        const currentFontSizeAttr = editor.getAttributes('textStyle').fontSize
+        if (typeof currentFontSizeAttr === 'string') {
+          const sizeValue = currentFontSizeAttr.replace('px', '')
+          setFontSize(sizeValue)
+        } else {
+          setFontSize('16')
+        }
+      }
+
+      updateFontSize()
+      editor.on('selectionUpdate', updateFontSize)
+      editor.on('transaction', updateFontSize)
+
+      return () => {
+        editor.off('selectionUpdate', updateFontSize)
+        editor.off('transaction', updateFontSize)
+      }
+    }, [editor])
+
+    // 개선된 executeCommand - 포커스 문제 해결 및 단순화
+    const executeCommand = useCallback(
+      (command: () => void) => {
+        if (!editor || disabled) return
+        
+        // 명령 실행 전 포커스 확보
+        if (!editor.isFocused) {
+          editor.commands.focus()
+        }
+        
+        // 명령 실행
+        command()
+        
+        // 명령 실행 후 포커스 유지
+        setTimeout(() => {
+          if (editor && !editor.isFocused) {
+            editor.commands.focus()
+          }
+        }, 10)
+      },
+      [editor, disabled]
+    )
+
+    // 폰트 사이즈 입력 필드의 동적 width 계산
+    const getFontSizeInputWidth = useCallback((size: string) => {
+      const length = size.length
+      return Math.max(length * 8 + 16, 40) // 최소 40px, 글자 수에 따라 증가
+    }, [])
+
+    if (!editor) return null
+
+    // --- Render toolbar UI ---
+    return (
+      <TooltipProvider>
+        <div className="border border-input bg-transparent rounded-t-md">
+          <div className="flex flex-wrap gap-1 p-1">
+            {/* 텍스트 스타일 */}
+            <Tooltip>
+              <TooltipTrigger asChild>
+                <Toggle
+                  size="sm"
+                  pressed={toolbarState.bold}
+                  onPressedChange={() =>
+                    executeCommand(() => editor.chain().focus().toggleBold().run())
+                  }
+                  disabled={disabled}
+                >
+                  <Bold className="h-4 w-4" />
+                </Toggle>
+              </TooltipTrigger>
+              <TooltipContent>
+                <p>굵게 (Ctrl+B)</p>
+              </TooltipContent>
+            </Tooltip>
+
+            <Tooltip>
+              <TooltipTrigger asChild>
+                <Toggle
+                  size="sm"
+                  pressed={toolbarState.italic}
+                  onPressedChange={() =>
+                    executeCommand(() => editor.chain().focus().toggleItalic().run())
+                  }
+                  disabled={disabled}
+                >
+                  <Italic className="h-4 w-4" />
+                </Toggle>
+              </TooltipTrigger>
+              <TooltipContent>
+                <p>기울임 (Ctrl+I)</p>
+              </TooltipContent>
+            </Tooltip>
+
+            <Tooltip>
+              <TooltipTrigger asChild>
+                <Toggle
+                  size="sm"
+                  pressed={toolbarState.underline}
+                  onPressedChange={() =>
+                    executeCommand(() => editor.chain().focus().toggleUnderline().run())
+                  }
+                  disabled={disabled}
+                >
+                  <UnderlineIcon className="h-4 w-4" />
+                </Toggle>
+              </TooltipTrigger>
+              <TooltipContent>
+                <p>밑줄</p>
+              </TooltipContent>
+            </Tooltip>
+
+            <Tooltip>
+              <TooltipTrigger asChild>
+                <Toggle
+                  size="sm"
+                  pressed={toolbarState.strike}
+                  onPressedChange={() =>
+                    executeCommand(() => editor.chain().focus().toggleStrike().run())
+                  }
+                  disabled={disabled}
+                >
+                  <Strikethrough className="h-4 w-4" />
+                </Toggle>
+              </TooltipTrigger>
+              <TooltipContent>
+                <p>취소선</p>
+              </TooltipContent>
+            </Tooltip>
+
+            <Separator orientation="vertical" className="h-6" />
+
+            {/* 제목 및 단락 */}
+            <DropdownMenu>
+              <DropdownMenuTrigger asChild>
+                <Toggle size="sm" pressed={toolbarState.heading} disabled={disabled}>
+                  <Type className="h-4 w-4" />
+                </Toggle>
+              </DropdownMenuTrigger>
+              <DropdownMenuContent align="start">
+                {([1, 2, 3] as Array<1 | 2 | 3>).map((level) => (
+                  <DropdownMenuItem
+                    key={level}
+                    onClick={() =>
+                      executeCommand(() =>
+                        editor.chain().focus().toggleHeading({ level }).run()
+                      )
+                    }
+                    className="flex items-center"
+                  >
+                    <span
+                      className={`font-bold ${level === 1 ? 'text-xl' : level === 2 ? 'text-lg' : 'text-base'
+                        }`}
+                    >
+                      제목 {level}
+                    </span>
+                  </DropdownMenuItem>
+                ))}
+                <DropdownMenuItem
+                  onClick={() =>
+                    executeCommand(() => editor.chain().focus().setParagraph().run())
+                  }
+                  className="flex items-center"
+                >
+                  <span>본문</span>
+                </DropdownMenuItem>
+              </DropdownMenuContent>
+            </DropdownMenu>
+
+            {/* 글자 크기 - 동적 width 적용 */}
+            <div className="flex items-center space-x-1">
+              <Input
+                type="number"
+                min="8"
+                max="72"
+                value={fontSize}
+                onChange={(e) => {
+                  const size = e.target.value
+                  setFontSize(size)
+                  if (size && parseInt(size) >= 8 && parseInt(size) <= 72) {
+                    executeCommand(() =>
+                      editor
+                        .chain()
+                        .focus()
+                        .setMark('textStyle', { fontSize: `${size}px` })
+                        .run()
+                    )
+                  }
+                }}
+                style={{ width: `${getFontSizeInputWidth(fontSize)}px` }}
+                className="h-8 text-xs"
+                disabled={disabled}
+              />
+              <DropdownMenu>
+                <DropdownMenuTrigger asChild>
+                  <Button variant="outline" size="sm" className="h-8 px-2" disabled={disabled}>
+                    <Type className="h-3 w-3" />
+                  </Button>
+                </DropdownMenuTrigger>
+                <DropdownMenuContent align="start">
+                  {[8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 32, 36, 48, 72].map((size) => (
+                    <DropdownMenuItem
+                      key={size}
+                      onClick={() => {
+                        setFontSize(size.toString())
+                        executeCommand(() =>
+                          editor
+                            .chain()
+                            .focus()
+                            .setMark('textStyle', { fontSize: `${size}px` })
+                            .run()
+                        )
+                      }}
+                      className="flex items-center"
+                    >
+                      <span style={{ fontSize: `${Math.min(size, 16)}px` }}>{size}px</span>
+                    </DropdownMenuItem>
+                  ))}
+                </DropdownMenuContent>
+              </DropdownMenu>
+            </div>
+
+            <Separator orientation="vertical" className="h-6" />
+
+            {/* 리스트 */}
+            <Tooltip>
+              <TooltipTrigger asChild>
+                <Toggle
+                  size="sm"
+                  pressed={toolbarState.bulletList}
+                  onPressedChange={() =>
+                    executeCommand(() => editor.chain().focus().toggleBulletList().run())
+                  }
+                  disabled={disabled}
+                >
+                  <List className="h-4 w-4" />
+                </Toggle>
+              </TooltipTrigger>
+              <TooltipContent>
+                <p>글머리 기호</p>
+              </TooltipContent>
+            </Tooltip>
+
+            <Tooltip>
+              <TooltipTrigger asChild>
+                <Toggle
+                  size="sm"
+                  pressed={toolbarState.orderedList}
+                  onPressedChange={() =>
+                    executeCommand(() => editor.chain().focus().toggleOrderedList().run())
+                  }
+                  disabled={disabled}
+                >
+                  <ListOrdered className="h-4 w-4" />
+                </Toggle>
+              </TooltipTrigger>
+              <TooltipContent>
+                <p>번호 매기기</p>
+              </TooltipContent>
+            </Tooltip>
+
+            <Tooltip>
+              <TooltipTrigger asChild>
+                <Toggle
+                  size="sm"
+                  pressed={toolbarState.blockquote}
+                  onPressedChange={() =>
+                    executeCommand(() => editor.chain().focus().toggleBlockquote().run())
+                  }
+                  disabled={disabled}
+                >
+                  <Quote className="h-4 w-4" />
+                </Toggle>
+              </TooltipTrigger>
+              <TooltipContent>
+                <p>인용문</p>
+              </TooltipContent>
+            </Tooltip>
+
+            <Separator orientation="vertical" className="h-6" />
+
+            {/* 텍스트 정렬 */}
+            <DropdownMenu>
+              <DropdownMenuTrigger asChild>
+                <Tooltip>
+                  <TooltipTrigger asChild>
+                    <Toggle size="sm" pressed={toolbarState.textAlign !== 'left'} disabled={disabled}>
+                      {toolbarState.textAlign === 'center' ? (
+                        <AlignCenter className="h-4 w-4" />
+                      ) : toolbarState.textAlign === 'right' ? (
+                        <AlignRight className="h-4 w-4" />
+                      ) : toolbarState.textAlign === 'justify' ? (
+                        <AlignJustify className="h-4 w-4" />
+                      ) : (
+                        <AlignLeft className="h-4 w-4" />
+                      )}
+                    </Toggle>
+                  </TooltipTrigger>
+                  <TooltipContent>
+                    <p>텍스트 정렬</p>
+                  </TooltipContent>
+                </Tooltip>
+              </DropdownMenuTrigger>
+              <DropdownMenuContent align="start">
+                <DropdownMenuItem
+                  onClick={() =>
+                    executeCommand(() => editor.chain().focus().setTextAlign('left').run())
+                  }
+                  className="flex items-center"
+                >
+                  <AlignLeft className="mr-2 h-4 w-4" />
+                  <span>왼쪽 정렬</span>
+                </DropdownMenuItem>
+                <DropdownMenuItem
+                  onClick={() =>
+                    executeCommand(() => editor.chain().focus().setTextAlign('center').run())
+                  }
+                  className="flex items-center"
+                >
+                  <AlignCenter className="mr-2 h-4 w-4" />
+                  <span>가운데 정렬</span>
+                </DropdownMenuItem>
+                <DropdownMenuItem
+                  onClick={() =>
+                    executeCommand(() => editor.chain().focus().setTextAlign('right').run())
+                  }
+                  className="flex items-center"
+                >
+                  <AlignRight className="mr-2 h-4 w-4" />
+                  <span>오른쪽 정렬</span>
+                </DropdownMenuItem>
+                <DropdownMenuItem
+                  onClick={() =>
+                    executeCommand(() => editor.chain().focus().setTextAlign('justify').run())
+                  }
+                  className="flex items-center"
+                >
+                  <AlignJustify className="mr-2 h-4 w-4" />
+                  <span>양쪽 정렬</span>
+                </DropdownMenuItem>
+              </DropdownMenuContent>
+            </DropdownMenu>
+
+            <Separator orientation="vertical" className="h-6" />
+
+            {/* 링크 */}
+            <Tooltip>
+              <TooltipTrigger asChild>
+                <Toggle
+                  size="sm"
+                  pressed={toolbarState.link}
+                  onPressedChange={() => {
+                    if (toolbarState.link) {
+                      executeCommand(() => editor.chain().focus().unsetLink().run())
+                    } else {
+                      const url = window.prompt('URL을 입력하세요:')
+                      if (url) {
+                        executeCommand(() => editor.chain().focus().setLink({ href: url }).run())
+                      }
+                    }
+                  }}
+                  disabled={disabled}
+                >
+                  <LinkIcon className="h-4 w-4" />
+                </Toggle>
+              </TooltipTrigger>
+              <TooltipContent>
+                <p>링크 {toolbarState.link ? '제거' : '삽입'}</p>
+              </TooltipContent>
+            </Tooltip>
+
+            {/* 이미지 업로드 */}
+            <Tooltip>
+              <TooltipTrigger asChild>
+                <div className="relative">
+                  <input
+                    type="file"
+                    accept="image/*"
+                    className="hidden"
+                    id="image-upload-rt"
+                    onChange={(e) => {
+                      const file = e.target.files?.[0]
+                      if (file) handleImageUpload(file)
+                    }}
+                  />
+                  <Toggle
+                    size="sm"
+                    pressed={false}
+                    onPressedChange={() => {
+                      document.getElementById('image-upload-rt')?.click()
+                    }}
+                    disabled={disabled}
+                  >
+                    <ImageIcon className="h-4 w-4" />
+                  </Toggle>
+                </div>
+              </TooltipTrigger>
+              <TooltipContent>
+                <p>이미지 삽입</p>
+              </TooltipContent>
+            </Tooltip>
+
+            <Separator orientation="vertical" className="h-6" />
+
+            {/* 첨자 */}
+            <Tooltip>
+              <TooltipTrigger asChild>
+                <Toggle
+                  size="sm"
+                  pressed={toolbarState.subscript}
+                  onPressedChange={() =>
+                    executeCommand(() => editor.chain().focus().toggleSubscript().run())
+                  }
+                  disabled={disabled}
+                >
+                  <SubscriptIcon className="h-4 w-4" />
+                </Toggle>
+              </TooltipTrigger>
+              <TooltipContent>
+                <p>아래 첨자</p>
+              </TooltipContent>
+            </Tooltip>
+
+            <Tooltip>
+              <TooltipTrigger asChild>
+                <Toggle
+                  size="sm"
+                  pressed={toolbarState.superscript}
+                  onPressedChange={() =>
+                    executeCommand(() => editor.chain().focus().toggleSuperscript().run())
+                  }
+                  disabled={disabled}
+                >
+                  <SuperscriptIcon className="h-4 w-4" />
+                </Toggle>
+              </TooltipTrigger>
+              <TooltipContent>
+                <p>위 첨자</p>
+              </TooltipContent>
+            </Tooltip>
+
+            <Separator orientation="vertical" className="h-6" />
+
+            {/* 하이라이트 */}
+            <Tooltip>
+              <TooltipTrigger asChild>
+                <Toggle
+                  size="sm"
+                  pressed={toolbarState.highlight}
+                  onPressedChange={() =>
+                    executeCommand(() => editor.chain().focus().toggleHighlight().run())
+                  }
+                  disabled={disabled}
+                >
+                  <Highlighter className="h-4 w-4" />
+                </Toggle>
+              </TooltipTrigger>
+              <TooltipContent>
+                <p>하이라이트</p>
+              </TooltipContent>
+            </Tooltip>
+
+            {/* 체크리스트 */}
+            <Tooltip>
+              <TooltipTrigger asChild>
+                <Toggle
+                  size="sm"
+                  pressed={toolbarState.taskList}
+                  onPressedChange={() =>
+                    executeCommand(() => editor.chain().focus().toggleTaskList().run())
+                  }
+                  disabled={disabled}
+                >
+                  <CheckSquare className="h-4 w-4" />
+                </Toggle>
+              </TooltipTrigger>
+              <TooltipContent>
+                <p>체크리스트</p>
+              </TooltipContent>
+            </Tooltip>
+
+            <Separator orientation="vertical" className="h-6" />
+
+            {/* 테이블 */}
+            {!toolbarState.table ? (
+              <Dialog open={isTableDialogOpen} onOpenChange={setIsTableDialogOpen}>
+                <DialogTrigger asChild>
+                  <Tooltip>
+                    <TooltipTrigger asChild>
+                      <Toggle
+                        size="sm"
+                        pressed={false}
+                        onPressedChange={() => {
+                          if (editor && editor.isActive('table')) {
+                            alert('커서를 테이블 밖으로 이동시키세요')
+                            return
+                          }
+                          setIsTableDialogOpen(true)
+                        }}
+                        disabled={disabled}
+                      >
+                        <TableIcon className="h-4 w-4" />
+                      </Toggle>
+                    </TooltipTrigger>
+                    <TooltipContent>
+                      <p>테이블 삽입</p>
+                    </TooltipContent>
+                  </Tooltip>
+                </DialogTrigger>
+                <DialogContent className="sm:max-w-md">
+                  <DialogHeader>
+                    <DialogTitle>테이블 크기 설정</DialogTitle>
+                    <DialogDescription>
+                      생성할 테이블의 행과 열 수를 입력하세요 (1-20)
+                    </DialogDescription>
+                  </DialogHeader>
+                  <div className="grid grid-cols-2 gap-4 py-4">
+                    <div className="space-y-2">
+                      <Label htmlFor="table-rows">행 수</Label>
+                      <Input
+                        id="table-rows"
+                        type="number"
+                        min="1"
+                        max="20"
+                        value={tableRows}
+                        onChange={(e) => setTableRows(e.target.value)}
+                        placeholder="3"
+                      />
+                    </div>
+                    <div className="space-y-2">
+                      <Label htmlFor="table-cols">열 수</Label>
+                      <Input
+                        id="table-cols"
+                        type="number"
+                        min="1"
+                        max="20"
+                        value={tableCols}
+                        onChange={(e) => setTableCols(e.target.value)}
+                        placeholder="3"
+                      />
+                    </div>
+                  </div>
+                  <DialogFooter>
+                    <Button variant="outline" onClick={() => setIsTableDialogOpen(false)}>
+                      취소
+                    </Button>
+                    <Button
+                      onClick={() => {
+                        const rows = parseInt(tableRows, 10)
+                        const cols = parseInt(tableCols, 10)
+                        if (rows >= 1 && rows <= 20 && cols >= 1 && cols <= 20) {
+                          executeCommand(() =>
+                            editor.chain().focus().insertTable({ rows, cols }).run()
+                          )
+                          setIsTableDialogOpen(false)
+                        }
+                      }}
+                      disabled={
+                        !tableRows ||
+                        !tableCols ||
+                        parseInt(tableRows, 10) < 1 ||
+                        parseInt(tableRows, 10) > 20 ||
+                        parseInt(tableCols, 10) < 1 ||
+                        parseInt(tableCols, 10) > 20
+                      }
+                    >
+                      생성
+                    </Button>
+                  </DialogFooter>
+                </DialogContent>
+              </Dialog>
+            ) : (
+              <DropdownMenu>
+                <DropdownMenuTrigger asChild>
+                  <Tooltip>
+                    <TooltipTrigger asChild>
+                      <Toggle size="sm" pressed={true} disabled={disabled}>
+                        <TableIcon className="h-4 w-4" />
+                      </Toggle>
+                    </TooltipTrigger>
+                    <TooltipContent>
+                      <p>테이블 편집</p>
+                    </TooltipContent>
+                  </Tooltip>
+                </DropdownMenuTrigger>
+                <DropdownMenuContent align="start">
+                  <DropdownMenuItem
+                    onClick={() =>
+                      executeCommand(() => editor.chain().focus().addRowBefore().run())
+                    }
+                    className="flex items-center"
+                  >
+                    <span>위에 행 추가</span>
+                  </DropdownMenuItem>
+                  <DropdownMenuItem
+                    onClick={() =>
+                      executeCommand(() => editor.chain().focus().addRowAfter().run())
+                    }
+                    className="flex items-center"
+                  >
+                    <span>아래에 행 추가</span>
+                  </DropdownMenuItem>
+                  <DropdownMenuItem
+                    onClick={() =>
+                      executeCommand(() => editor.chain().focus().addColumnBefore().run())
+                    }
+                    className="flex items-center"
+                  >
+                    <span>왼쪽에 열 추가</span>
+                  </DropdownMenuItem>
+                  <DropdownMenuItem
+                    onClick={() =>
+                      executeCommand(() => editor.chain().focus().addColumnAfter().run())
+                    }
+                    className="flex items-center"
+                  >
+                    <span>오른쪽에 열 추가</span>
+                  </DropdownMenuItem>
+                  <DropdownMenuItem
+                    onClick={() =>
+                      executeCommand(() => editor.chain().focus().deleteRow().run())
+                    }
+                    className="flex items-center"
+                  >
+                    <span>행 삭제</span>
+                  </DropdownMenuItem>
+                  <DropdownMenuItem
+                    onClick={() =>
+                      executeCommand(() => editor.chain().focus().deleteColumn().run())
+                    }
+                    className="flex items-center"
+                  >
+                    <span>열 삭제</span>
+                  </DropdownMenuItem>
+                  <DropdownMenuItem
+                    onClick={() =>
+                      executeCommand(() => editor.chain().focus().deleteTable().run())
+                    }
+                    className="flex items-center text-red-600"
+                  >
+                    <span>테이블 삭제</span>
+                  </DropdownMenuItem>
+                </DropdownMenuContent>
+              </DropdownMenu>
+            )}
+
+            <Separator orientation="vertical" className="h-6" />
+
+            {/* 실행 취소/다시 실행 */}
+            <Tooltip>
+              <TooltipTrigger asChild>
+                <Toggle
+                  size="sm"
+                  pressed={false}
+                  onPressedChange={() =>
+                    executeCommand(() => editor.chain().focus().undo().run())
+                  }
+                  disabled={!editor.can().undo() || disabled}
+                >
+                  <Undo className="h-4 w-4" />
+                </Toggle>
+              </TooltipTrigger>
+              <TooltipContent>
+                <p>실행 취소 (Ctrl+Z)</p>
+              </TooltipContent>
+            </Tooltip>
+
+            <Tooltip>
+              <TooltipTrigger asChild>
+                <Toggle
+                  size="sm"
+                  pressed={false}
+                  onPressedChange={() =>
+                    executeCommand(() => editor.chain().focus().redo().run())
+                  }
+                  disabled={!editor.can().redo() || disabled}
+                >
+                  <Redo className="h-4 w-4" />
+                </Toggle>
+              </TooltipTrigger>
+              <TooltipContent>
+                <p>다시 실행 (Ctrl+Y)</p>
+              </TooltipContent>
+            </Tooltip>
+          </div>
+        </div>
+      </TooltipProvider>
+    )
+  }
+
+  // ---------------------------------------------------------------------------
+  // Layout & rendering
+  // ---------------------------------------------------------------------------
+  const containerStyle = height === '100%' ? { height: '100%' } : { height }
+  const editorContentStyle =
+    height === '100%' ? { flex: 1, minHeight: 0 } : { height: `calc(${height} - 60px)` }
+
+  return (
+    <div
+      className={`border rounded-md bg-background ${height === '100%' ? 'flex flex-col h-full' : ''}`}
+      style={containerStyle}
+    >
+      <div className="flex-shrink-0 border-b">
+        <Toolbar editor={editor} disabled={disabled} />
+      </div>
+      <div className="overflow-y-auto" style={editorContentStyle}>
+        <EditorContent editor={editor} className="h-full" />
+      </div>
+    </div>
+  )
+} 
\ No newline at end of file
-- 
cgit v1.2.3

