기능구현중
This commit is contained in:
220
frontend/components/common/TiptapEditor.vue
Normal file
220
frontend/components/common/TiptapEditor.vue
Normal file
@@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<div class="tiptap-editor">
|
||||
<!-- 툴바 -->
|
||||
<div v-if="showToolbar && editor" class="tiptap-toolbar border-bottom bg-light px-2 py-1">
|
||||
<div class="btn-group btn-group-sm me-2">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
:class="{ active: editor.isActive('bold') }"
|
||||
@click="editor.chain().focus().toggleBold().run()"
|
||||
title="굵게 (Ctrl+B)">
|
||||
<i class="bi bi-type-bold"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
:class="{ active: editor.isActive('italic') }"
|
||||
@click="editor.chain().focus().toggleItalic().run()"
|
||||
title="기울임 (Ctrl+I)">
|
||||
<i class="bi bi-type-italic"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
:class="{ active: editor.isActive('strike') }"
|
||||
@click="editor.chain().focus().toggleStrike().run()"
|
||||
title="취소선">
|
||||
<i class="bi bi-type-strikethrough"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group btn-group-sm me-2">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
:class="{ active: editor.isActive('heading', { level: 2 }) }"
|
||||
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
|
||||
title="제목">
|
||||
<i class="bi bi-type-h2"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
:class="{ active: editor.isActive('heading', { level: 3 }) }"
|
||||
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
|
||||
title="소제목">
|
||||
<i class="bi bi-type-h3"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group btn-group-sm me-2">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
:class="{ active: editor.isActive('bulletList') }"
|
||||
@click="editor.chain().focus().toggleBulletList().run()"
|
||||
title="글머리 기호">
|
||||
<i class="bi bi-list-ul"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
:class="{ active: editor.isActive('orderedList') }"
|
||||
@click="editor.chain().focus().toggleOrderedList().run()"
|
||||
title="번호 목록">
|
||||
<i class="bi bi-list-ol"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
:class="{ active: editor.isActive('blockquote') }"
|
||||
@click="editor.chain().focus().toggleBlockquote().run()"
|
||||
title="인용">
|
||||
<i class="bi bi-quote"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
@click="editor.chain().focus().setHorizontalRule().run()"
|
||||
title="구분선">
|
||||
<i class="bi bi-hr"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
@click="editor.chain().focus().undo().run()"
|
||||
:disabled="!editor.can().undo()"
|
||||
title="실행 취소 (Ctrl+Z)">
|
||||
<i class="bi bi-arrow-counterclockwise"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
@click="editor.chain().focus().redo().run()"
|
||||
:disabled="!editor.can().redo()"
|
||||
title="다시 실행 (Ctrl+Y)">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 에디터 본문 -->
|
||||
<EditorContent :editor="editor" class="tiptap-content" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useEditor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import Link from '@tiptap/extension-link'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
placeholder?: string
|
||||
showToolbar?: boolean
|
||||
minHeight?: string
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const editor = useEditor({
|
||||
content: props.modelValue || '',
|
||||
editable: !props.readonly,
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
heading: {
|
||||
levels: [2, 3]
|
||||
}
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: props.placeholder || '내용을 입력하세요...'
|
||||
}),
|
||||
Link.configure({
|
||||
openOnClick: false
|
||||
})
|
||||
],
|
||||
onUpdate: ({ editor }) => {
|
||||
emit('update:modelValue', editor.getHTML())
|
||||
}
|
||||
})
|
||||
|
||||
// props 변경 감지
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
if (editor.value && newValue !== editor.value.getHTML()) {
|
||||
editor.value.commands.setContent(newValue || '')
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.readonly, (newValue) => {
|
||||
if (editor.value) {
|
||||
editor.value.setEditable(!newValue)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
editor.value?.destroy()
|
||||
})
|
||||
|
||||
// 외부에서 접근 가능한 메서드
|
||||
defineExpose({
|
||||
getHTML: () => editor.value?.getHTML() || '',
|
||||
getText: () => editor.value?.getText() || '',
|
||||
focus: () => editor.value?.commands.focus(),
|
||||
clear: () => editor.value?.commands.clearContent()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tiptap-editor {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tiptap-toolbar .btn.active {
|
||||
background-color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tiptap-content {
|
||||
min-height: v-bind('minHeight || "300px"');
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.tiptap-content :deep(.tiptap) {
|
||||
outline: none;
|
||||
min-height: inherit;
|
||||
}
|
||||
|
||||
.tiptap-content :deep(.tiptap p.is-editor-empty:first-child::before) {
|
||||
color: #adb5bd;
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tiptap-content :deep(.tiptap h2) {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.tiptap-content :deep(.tiptap h3) {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tiptap-content :deep(.tiptap ul),
|
||||
.tiptap-content :deep(.tiptap ol) {
|
||||
padding-left: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tiptap-content :deep(.tiptap blockquote) {
|
||||
border-left: 3px solid #dee2e6;
|
||||
padding-left: 1rem;
|
||||
margin-left: 0;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.tiptap-content :deep(.tiptap hr) {
|
||||
border: none;
|
||||
border-top: 1px solid #dee2e6;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.tiptap-content :deep(.tiptap p) {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
@@ -146,13 +146,14 @@
|
||||
<strong>회의 내용</strong>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<textarea
|
||||
<TiptapEditor
|
||||
v-if="isEditing"
|
||||
class="form-control border-0 h-100"
|
||||
v-model="form.rawContent"
|
||||
style="min-height: 300px; resize: none;"
|
||||
></textarea>
|
||||
<div v-else class="p-3" style="min-height: 200px; white-space: pre-wrap;">{{ meeting.rawContent || '(내용 없음)' }}</div>
|
||||
:show-toolbar="true"
|
||||
min-height="300px"
|
||||
placeholder="회의 내용을 입력하세요..."
|
||||
/>
|
||||
<div v-else class="p-3 meeting-content" style="min-height: 200px;" v-html="meeting.rawContent || '<span class=\'text-muted\'>(내용 없음)</span>'"></div>
|
||||
</div>
|
||||
<div v-if="isEditing" class="card-footer d-flex justify-content-end">
|
||||
<button class="btn btn-secondary me-2" @click="cancelEdit">취소</button>
|
||||
@@ -600,4 +601,35 @@ function getStatusText(status: string) {
|
||||
.modal.show {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.meeting-content :deep(h2) {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.meeting-content :deep(h3) {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.meeting-content :deep(ul),
|
||||
.meeting-content :deep(ol) {
|
||||
padding-left: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.meeting-content :deep(blockquote) {
|
||||
border-left: 3px solid #dee2e6;
|
||||
padding-left: 1rem;
|
||||
margin-left: 0;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.meeting-content :deep(p) {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -121,20 +121,18 @@
|
||||
<small class="text-muted ms-2">(자유롭게 작성하면 AI가 정리해줍니다)</small>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<!-- TODO: Tiptap 에디터로 교체 -->
|
||||
<textarea
|
||||
class="form-control border-0 h-100"
|
||||
<TiptapEditor
|
||||
v-model="form.rawContent"
|
||||
:show-toolbar="true"
|
||||
min-height="500px"
|
||||
placeholder="회의 내용을 자유롭게 작성하세요...
|
||||
|
||||
예시:
|
||||
- 김철수: 이번 주 진행 상황 공유
|
||||
- 프론트엔드 개발 80% 완료
|
||||
- 백엔드 API 연동 필요
|
||||
- 다음 주 목표: 테스트 환경 구축
|
||||
- 미결정: 배포 일정 (추후 논의)"
|
||||
style="min-height: 500px; resize: none;"
|
||||
></textarea>
|
||||
- 다음 주 목표: 테스트 환경 구축"
|
||||
/>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<button class="btn btn-secondary me-2" @click="router.push('/meeting')">취소</button>
|
||||
|
||||
Reference in New Issue
Block a user