diff --git a/components/TextEditor.tsx b/components/TextEditor.tsx
new file mode 100644
index 0000000..d3e0be7
--- /dev/null
+++ b/components/TextEditor.tsx
@@ -0,0 +1,49 @@
+import dynamic from 'next/dynamic';
+
+import 'react-quill-new/dist/quill.snow.css';
+
+// ref: https://www.npmjs.com/package/react-quill-new
+const QuillEditor = dynamic(() => import('react-quill-new'), {
+ ssr: false,
+ loading: () =>
편집기 불러오는 중...
,
+});
+
+interface Props {
+ value?: string;
+ onChange: (value: string) => void;
+}
+
+/**
+ * 텍스트 에디터 컴포넌트
+ * @param {object} props
+ * @param {string} props.value - 초기 값
+ * @param {function} props.onChange - 값 변경시 콜백 함수
+ */
+export default function TextEditor({ value = '', onChange }: Props) {
+ const modules = {
+ toolbar: {
+ container: [
+ [{ header: [1, 2, 3, false] }],
+ ['bold', 'italic', 'underline'],
+ [{ align: null }, { align: 'center' }, { align: 'right' }],
+ [{ list: 'ordered' }, { list: 'bullet' }],
+ ['blockquote', 'link', 'image'],
+ ],
+ },
+ };
+
+ const handleChange = (value: string) => {
+ onChange(value);
+ };
+
+ return (
+
+ );
+}
diff --git a/package-lock.json b/package-lock.json
index 765a356..c55d131 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,7 +14,8 @@
"next": "^15.1.0",
"postcss-nesting": "^13.0.1",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "react-quill-new": "^3.3.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
@@ -2897,6 +2898,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+ "license": "MIT"
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -2908,7 +2915,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
- "dev": true,
"license": "Apache-2.0"
},
"node_modules/fast-glob": {
@@ -4091,6 +4097,30 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash-es": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.clonedeep": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+ "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
+ "license": "MIT"
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -4543,6 +4573,12 @@
"dev": true,
"license": "BlueOak-1.0.0"
},
+ "node_modules/parchment": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz",
+ "integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -5047,6 +5083,35 @@
],
"license": "MIT"
},
+ "node_modules/quill": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz",
+ "integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "eventemitter3": "^5.0.1",
+ "lodash-es": "^4.17.21",
+ "parchment": "^3.0.0",
+ "quill-delta": "^5.1.0"
+ },
+ "engines": {
+ "npm": ">=8.2.3"
+ }
+ },
+ "node_modules/quill-delta": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz",
+ "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-diff": "^1.3.0",
+ "lodash.clonedeep": "^4.5.0",
+ "lodash.isequal": "^4.5.0"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ }
+ },
"node_modules/react": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
@@ -5075,6 +5140,21 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/react-quill-new": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/react-quill-new/-/react-quill-new-3.3.3.tgz",
+ "integrity": "sha512-jxbm1QUJlkuGUpc9/GUgGw5USLHdp43H0M7AufqS3V+zRLng9uqLeVBGjXYqEbUKi8QVOM4SClSV3F7kVNj68w==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.21",
+ "quill": "~2.0.2"
+ },
+ "peerDependencies": {
+ "quill-delta": "^5.1.0",
+ "react": "^16 || ^17 || ^18 || ^19",
+ "react-dom": "^16 || ^17 || ^18 || ^19"
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
diff --git a/package.json b/package.json
index 0019e92..b853582 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,8 @@
"next": "^15.1.0",
"postcss-nesting": "^13.0.1",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "react-quill-new": "^3.3.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
diff --git a/pages/test/editor.tsx b/pages/test/editor.tsx
new file mode 100644
index 0000000..63064b8
--- /dev/null
+++ b/pages/test/editor.tsx
@@ -0,0 +1,52 @@
+import { useState } from 'react';
+
+import TextEditor from '@/components/TextEditor';
+
+const cellStyle = 'px-4 py-2';
+const trStyle = 'border-b';
+
+export default function Editor() {
+ const [value, setValue] = useState('');
+
+ const handleChange = (v: string) => {
+ // console.log('value', v);
+ setValue(v);
+ };
+
+ return (
+
+
+
+
+ |
+ props
+ |
+
+ example
+ |
+
+
+
+
+
+
+ - value: string
+ - onChange: (value: string) => void
+
+ |
+
+
+
+
+
+
+ 참고: 에디터의 크기는 에디터 부모의 100%가 적용되니 부모의
+ 크기를 정의하시면 됩니다.
+
+ |
+
+
+
+
+ );
+}
diff --git a/styles/globals.css b/styles/globals.css
index 5a6ef0a..647f854 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -51,3 +51,36 @@ body {
color: var(--gray-500);
background: var(--background);
}
+
+/* quill editor custom style */
+.quill-custom {
+ @apply grid h-full w-full;
+ grid-template-rows: 1fr max-content;
+
+ .ql-editor {
+ @apply p-0;
+ }
+ .ql-editor.ql-blank::before {
+ @apply left-0 not-italic text-gray-400;
+ }
+ .ql-container {
+ @apply overflow-y-auto font-sans text-16;
+ }
+ .ql-container.ql-snow {
+ @apply border-0;
+ }
+ .ql-toolbar.ql-snow {
+ @apply order-last rounded-full border-gray-200 text-gray-400;
+ }
+ .ql-snow .ql-stroke {
+ stroke: var(--gray-400);
+ }
+ .ql-snow .ql-fill,
+ .ql-snow .ql-stroke.ql-fill {
+ fill: var(--gray-400);
+ }
+ .ql-snow .ql-picker.ql-header .ql-picker-label::before,
+ .ql-snow .ql-picker.ql-header .ql-picker-item::before {
+ color: var(--gray-400);
+ }
+}