나는 Vue3를 쓰기 때문에
구글링하다가 <Editor /> 형식으로 사용하는 것들은 적용할 수 없다고 한다. ^^..
ref 써야하고 ..
npm install @toast-ui/editor
이거.. /뒤에 vue까지 쓰는 건 vue2방식이니 주의 .....
import Editor from '@toast-ui/editor';
import '@toast-ui/editor/dist/toastui-editor.css';
css도 같이 npm install 하고 ... import 해주고 ..
참고한 블로그
[Vue3]tui-editor vue3 wrapper
2025-10-15 추가.아래 Options API로 작성된 tui-editor wrapper를 이번에 사용할 일이 있어서 Composition로 바꿔 보았다.아래 소스를 사용하기 위해서는 아래 패키지를 먼저 설치 해야 된다. 뭐 또 언젠간 쓸
blog.gizmo80.com
어 제 는 ~ 분명히 부모 컴포넌트에서 open 함수 여는 코드로 안돌아갔는데 .^^. 오늘 하니까 갑자기 또 됨 ㅎ.ㅠ
watch, nextTick 안쓰고 구현하기로 변경 ...
watch, nextTick 없이 구현
<부모>
<NoticeCustsEditor ref="custsEditorDialogRef"/>
const custsEditorDialogRef = ref(null);
// 거래처 에디터 열기
const custsEditorOpen = () => {
custsEditorDialogRef.value.open("띄울 내용 넘겨야함")
}
<자식>
<template>
<!-- 공지사항 -->
<v-dialog v-model="localValue" max-width="800">
<v-card>
<v-card-title>거래처 공지사항</v-card-title>
<v-card-text>
<v-autocomplete
label="거래처"
multiple
density="compact"
variant="outlined"
clearable
keydown.enter=""
style="width:400px;">
</v-autocomplete>
<div class="d-flex align-center" style="gap: 8px;">
<v-text-field
v-model="title"
label="제목"
density="compact"
variant="outlined"
clearable
keydown.enter="">
</v-text-field>
<div class="d-flex align-center" style="flex-shrink: 0; gap: 8px;">
<input type="file" multiple ref="fileInput" @change="onFileChange" style="display: none;" />
<v-btn
class = "mb-6"
color="secondary"
density="default"
@click="triggerFileInput">
파일 선택
</v-btn>
<span class = "mb-6">{{selectedFileName || '선택된 파일 없음'}}</span>
</div>
</div>
<div ref="tuiEditor" style="height:500px;"></div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" text @click="close">취소</v-btn>
<v-btn color="primary" text @click="close">등록</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup>
import "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css";
import "tui-color-picker/dist/tui-color-picker.css";
import "@toast-ui/editor-plugin-color-syntax/dist/toastui-editor-plugin-color-syntax.css";
import '@toast-ui/editor/dist/toastui-editor.css';
import '@toast-ui/editor/dist/toastui-editor-viewer.css';
import "prismjs/themes/prism.css";
import colorSyntax from "@toast-ui/editor-plugin-color-syntax";
import Prism from "prismjs";
import "prismjs/components/prism-c";
import "prismjs/components/prism-cpp";
import "prismjs/components/prism-java";
import "prismjs/components/prism-python";
import "@toast-ui/editor/dist/i18n/ko-kr";
import { Editor } from "@toast-ui/editor";
import codeSyntaxHighlight from "@toast-ui/editor-plugin-code-syntax-highlight";
import { ref } from "vue";
import { consoleLog } from "@/assets/js/common/common.js";
import {_t} from "@/assets/js/common/common.js";
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
modelValue: Boolean,
content: { type: String, default: "" },
});
const localValue = ref(false);
const tuiEditor = ref(null);
let editor = null;
let editorText = ref("");
const title = ref("");
const fileInput = ref(null);
const selectedFileName = ref("");
// 파일 선택
const triggerFileInput = () => {
fileInput.value.click();
};
// 파일 변경
const onFileChange = (event) => {
const files = event.target.files;
if (files.length > 0) {
if (files.length === 1) {
selectedFileName.value = files[0].name;
} else {
selectedFileName.value = `파일 ${files.length}개 선택`;
}
} else {
selectedFileName.value = '';
}
};
// Editor 생성
const setEditor = (content) => {
if(editor) {
editor.destroy();
editor = null;
}
editor = new Editor.factory({
el: tuiEditor.value,
height: "500px",
viewer: false,
toolbarItems: [
["heading", "bold", "italic", "strike"],
["hr", "quote"],
["ul", "ol", "task", "indent", "outdent"],
["table", "image", "link"],
["code", "codeblock"],
["scrollSync"],
],
plugins: [colorSyntax, [codeSyntaxHighlight, { highlighter: Prism }]],
initialValue: content || "",
initialEditType: "wysiwyg",
language: "ko-KR",
events: {
change: onChangeEditor,
},
})
};
// 내용 변경 시 부모로 emit
const onChangeEditor = () => {
editorText.value = editor.getHTML();
emit("update:modelValue", editorText.value);
};
const open = (content = "") => {
localValue.value = true;
consoleLog("공지 open()", content);
// DOM 렌더링 직후 editor 초기화
setTimeout(() => {
if (tuiEditor.value) {
setEditor(content);
}
}, 0);
};
const close = () => {
localValue.value = false;
if (editor) {
editor.destroy();
editor = null;
}
};
const save = () => {
console.log("NoticeEditor 저장:", editorText.value);
close();
};
defineExpose({
open,
close,
getContents: () => editorText.value,
});
</script>
watch, nextTick 써서 구현
<부모>
<NoticeCustsEditor ref="custsEditorDialogRef" v-model="custsEditorDialog" />
const custsEditorDialog = ref(false);
// 거래처 에디터 열기
const custsEditorOpen = () => {
custsEditorDialog.value = true;
}
<자식>
<template>
<!-- 공지사항 -->
<v-dialog v-model="localValue" max-width="800">
<v-card>
<v-card-title>
전체 공지사항
</v-card-title>
<v-card-text>
<div class="d-flex align-center mb-4" style="gap: 8px;">
<v-text-field
label="제목"
density="compact"
variant="outlined"
clearable
keydown.enter="">
</v-text-field>
<div class="d-flex align-center" style="flex-shrink: 0; gap: 8px;">
<input type="file" multiple ref="fileInput" @change="onFileChange" style="display: none;" />
<v-btn
class = "mb-6"
color="secondary"
density="default"
@click="triggerFileInput">
파일 선택
</v-btn>
<span class = "mb-6">{{selectedFileName || '선택된 파일 없음'}}</span>
</div>
</div>
<div ref="tuiEditor" style="height:500px;"></div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" text @click="close">취소</v-btn>
<v-btn color="primary" text @click="close">수정</v-btn>
<v-btn color="primary" text @click="close">등록</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup>
import { ref, watch, nextTick } from "vue";
import Editor from "@toast-ui/editor";
import "@toast-ui/editor/dist/toastui-editor.css";
import "@toast-ui/editor/dist/toastui-editor-viewer.css";
import {_t} from "@/assets/js/common/common.js";
const props = defineProps({
modelValue: Boolean,
content: { type: String, default: "" },
});
const emit = defineEmits(["update:modelValue"]);
const tuiEditor = ref(null);
let editorInstance = null;
const localValue = ref(props.modelValue);
const fileInput = ref(null);
const selectedFileName = ref('');
const triggerFileInput = () => {
fileInput.value.click();
};
const onFileChange = (event) => {
const files = event.target.files;
if (files.length > 0) {
if (files.length === 1) {
selectedFileName.value = files[0].name;
} else {
selectedFileName.value = `파일 ${files.length}개 선택`;
}
} else {
selectedFileName.value = '';
}
};
// props -> local 동기화
watch(
() => props.modelValue,
(val) => {
localValue.value = val;
if (val) {
nextTick(() => {
if (tuiEditor.value && !editorInstance) {
editorInstance = new Editor({
el: tuiEditor.value,
height: "500px",
initialEditType: "wysiwyg", // "markdown"
previewStyle: "vertical",
initialValue: props.content,
});
} else if (editorInstance) {
editorInstance.setHTML(props.content);
}
});
}
else {
if (editorInstance) {
// editor.destroy()를 호출하여 메모리 해제 및 DOM 정리
editorInstance.destroy();
editorInstance = null; // 인스턴스 레퍼런스 초기화
}
}
}
);
const close = () => {
// 닫기 버튼을 누르면 watch가 val: false를 감지하고 destroy 로직을 실행
localValue.value = false;
emit("update:modelValue", false);
};
const getContents = () => editorInstance ? editorInstance.getHTML() : "";
defineExpose({
open: (content) => {
localValue.value = true;
},
getContents,
});
</script>
<style scoped>
</style>
TuiEditor 컴포넌트 따로 빼서 사용
NoticeEditor.vue
<template>
<!-- 전체 공지 에디터 -->
<v-dialog v-model="isOpen" max-width="800">
<v-card>
<v-card-title>전체 공지사항 등록수정</v-card-title>
<v-card-text>
<div class="d-flex align-center mb-4" style="gap: 8px;">
<v-text-field
v-model="title"
label="제목"
density="compact"
variant="outlined"
clearable
keydown.enter="">
</v-text-field>
<div class="d-flex align-center" style="flex-shrink: 0; gap: 8px;">
<v-btn color="primary mb-6" @click="openFileDialog">
첨부파일
</v-btn>
</div>
</div>
<TuiEditor ref="editor" :content="contents" :isViewer="isViewer"/>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" text @click="close">닫기</v-btn>
<v-btn v-if="!isViewer" color="primary" text @click="save">등록</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<AddFileDialog ref="addFileDialogRef" @file-saved="" />
</template>
<script setup>
import {ref} from "vue";
import {consoleLog} from "@/assets/js/common/common.js";
import AddFileDialog from "@/components/common/dialog/AddFileDialog.vue";
import TuiEditor from "@/components/common/editor/TuiEditor.vue";
const props = defineProps({
content: { type: String, default: "" },
isViewer: { type: Boolean, default: false },
});
const emit = defineEmits(["update:modelValue"]);
let isOpen = ref(false); // dialog 상태
// AddFileDialog
let addFileDialogRef = ref();
// TuiEditor
let editor = ref();
let isViewer = ref(false); // viewer, editor
let title = ref("")
let contents = ref("")
// AddFileDialog.vue 열기
const openFileDialog = () => {
const fileSeq = 1;
addFileDialogRef.value?.open(fileSeq);
}
// 열기
const open = (data = { title:"", isViewer: false, content:"" }) => {
consoleLog("전체 공지 open");
isOpen.value = true;
title = data.title;
isViewer = data.isViewer;
contents = data.content;
};
// 닫기
const close = () => {
isOpen.value = false;
};
// 저장
const save = () => {
consoleLog("NoticeEditor 저장:", editor.value.getContents());
emit("update:modelValue", editor.value.getContents());
close();
};
// 수정
const doEditMode = () => {
editor.value.isEditMode(false)
}
// const getData = (value) => {
// consoleLog("getData", value)
// }
defineExpose({
open
});
</script>
<style scoped>
</style>
TuiEditor.vue
<template>
<div ref="tuiEditor"></div>
</template>
<script setup>
import "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css";
import "tui-color-picker/dist/tui-color-picker.css";
import "@toast-ui/editor-plugin-color-syntax/dist/toastui-editor-plugin-color-syntax.css";
import '@toast-ui/editor/dist/toastui-editor.css';
import '@toast-ui/editor/dist/toastui-editor-viewer.css';
import "prismjs/themes/prism.css";
import colorSyntax from "@toast-ui/editor-plugin-color-syntax";
import Prism from "prismjs";
import "prismjs/components/prism-c";
import "prismjs/components/prism-cpp";
import "prismjs/components/prism-java";
import "prismjs/components/prism-python";
import "@toast-ui/editor/dist/i18n/ko-kr";
import codeSyntaxHighlight from "@toast-ui/editor-plugin-code-syntax-highlight";
import { ref, onMounted } from "vue";
import { consoleLog } from "@/assets/js/common/common.js";
import { Editor } from "@toast-ui/editor";
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
content: {type: String, default: "" },
isViewer: {type: Boolean, default: false},
})
let tuiEditor = ref();
let editorText = ref("");
let editor = null
// Editor 생성
const setEditor = (isViewer) => {
if (editor) {
editor.destroy();
editor = null;
}
console.log("isViewer", isViewer);
editor = new Editor.factory({
el: tuiEditor.value,
height: "500px",
viewer: isViewer,
toolbarItems: [
["heading", "bold", "italic", "strike"],
["hr", "quote"],
["ul", "ol", "task", "indent", "outdent"],
["table", "image", "link"],
["code", "codeblock"],
["scrollSync"],
],
plugins: [colorSyntax, [codeSyntaxHighlight, {highlighter: Prism}]],
initialValue: props.content || "",
initialEditType: "wysiwyg", // "markdown"
language: "ko-KR",
events: {
change: onChangeEditor,
},
});
}
// 에디터 내용 변경 시 부모한테 emit
const onChangeEditor = () => {
editorText.value = editor.getHTML()
emit("update:modelValue", editorText.value)
}
// 에디터 객체 생성하는 초기화 함수.
const doInit = () => {
setEditor(props.isViewer)
console.log("doInit", props.isViewer);
}
const isEditMode = (isViewer) => {
editor.destroy()
setEditor(isViewer);
}
/**
* 부모가 자식의 현재 콘텐츠 직접 가져오도록 하는 getter.
* editorRef.value.getContents() 형태로 호출.
*/
const getContents = () => {
return editorText.value
}
onMounted(() => {
doInit()
})
defineExpose({
isEditMode,
getContents,
})
</script>
<style scoped>
</style>