Skip to content

Commit 9720a32

Browse files
committed
하이퍼링크 추가
1 parent 22a5aa0 commit 9720a32

4 files changed

Lines changed: 240 additions & 16 deletions

File tree

sciq-fe/node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts

Lines changed: 118 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<script setup lang="ts">
2+
import { ref, onMounted, watch, defineEmits, defineProps } from 'vue';
3+
import 'quill/dist/quill.snow.css';
4+
5+
const props = defineProps({
6+
modelValue: {
7+
type: String,
8+
default: ''
9+
},
10+
placeholder: {
11+
type: String,
12+
default: '내용을 입력하세요'
13+
}
14+
});
15+
16+
const emit = defineEmits(['update:modelValue']);
17+
18+
const editorContainer = ref<HTMLElement | null>(null);
19+
let quill: any = null;
20+
21+
onMounted(async () => {
22+
if (typeof window !== 'undefined' && editorContainer.value) {
23+
const Quill = (await import('quill')).default;
24+
25+
// 에디터 초기화
26+
quill = new Quill(editorContainer.value, {
27+
theme: 'snow',
28+
placeholder: props.placeholder,
29+
modules: {
30+
toolbar: [
31+
['bold', 'italic', 'underline', 'strike'],
32+
['blockquote', 'code-block'],
33+
[{ 'header': 1 }, { 'header': 2 }],
34+
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
35+
[{ 'script': 'sub' }, { 'script': 'super' }],
36+
[{ 'indent': '-1' }, { 'indent': '+1' }],
37+
[{ 'direction': 'rtl' }],
38+
[{ 'size': ['small', false, 'large', 'huge'] }],
39+
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
40+
[{ 'color': [] }, { 'background': [] }],
41+
[{ 'font': [] }],
42+
[{ 'align': [] }],
43+
['clean'],
44+
['link', 'image']
45+
]
46+
}
47+
});
48+
49+
// 초기값 설정
50+
if (props.modelValue) {
51+
quill.root.innerHTML = props.modelValue;
52+
}
53+
54+
// 에디터 변경 이벤트 감시
55+
quill.on('text-change', () => {
56+
emit('update:modelValue', quill.root.innerHTML);
57+
});
58+
}
59+
});
60+
61+
// 외부에서 값이 변경될 경우 에디터에 반영
62+
watch(() => props.modelValue, (newVal) => {
63+
if (quill && newVal !== quill.root.innerHTML) {
64+
quill.root.innerHTML = newVal;
65+
}
66+
});
67+
</script>
68+
69+
<template>
70+
<div class="rich-text-editor">
71+
<div ref="editorContainer" class="editor-container"></div>
72+
</div>
73+
</template>
74+
75+
<style scoped>
76+
.rich-text-editor {
77+
width: 100%;
78+
}
79+
80+
.editor-container {
81+
height: 300px;
82+
overflow-y: auto;
83+
border-radius: 4px;
84+
}
85+
86+
:deep(.ql-toolbar) {
87+
border-top-left-radius: 4px;
88+
border-top-right-radius: 4px;
89+
background-color: #f8f9fa;
90+
}
91+
92+
:deep(.ql-container) {
93+
border-bottom-left-radius: 4px;
94+
border-bottom-right-radius: 4px;
95+
font-size: 16px;
96+
line-height: 1.5;
97+
}
98+
99+
:deep(.ql-editor) {
100+
min-height: 250px;
101+
}
102+
</style>

sciq-fe/src/pages/Board/BoardDetail.vue

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,9 @@
5555
</div>
5656
</div>
5757

58-
<div class="content" v-if="!isEditing" v-html="question.content"></div>
58+
<div class="content" v-if="!isEditing" v-html="formatContentWithLinks(question.content)"></div>
5959
<div v-else class="edit-form">
60-
<textarea
61-
v-model="editContent"
62-
class="edit-content"
63-
placeholder="내용을 입력하세요"
64-
></textarea>
60+
<RichTextEditor v-model="editContent" placeholder="내용을 입력하세요" />
6561
<div class="char-count">{{ editContent.length }}/1000</div>
6662
</div>
6763

@@ -155,6 +151,7 @@ import { questionService } from '@/api/questionService'
155151
import { authService } from '@/api/authService'
156152
import type { Question, Comment, CommentCreateRequest, CommentUpdateRequest } from '@/types/board'
157153
import { ScienceDisciplineType } from '@/types/board'
154+
import RichTextEditor from '@/components/editor/RichTextEditor.vue'
158155
159156
const router = useRouter()
160157
const route = useRoute()
@@ -573,6 +570,19 @@ const deleteComment = async (commentId: number) => {
573570
}
574571
}
575572
573+
// URL을 자동으로 하이퍼링크로 변환하는 함수
574+
const formatContentWithLinks = (content: string) => {
575+
if (!content) return '';
576+
577+
// URL 패턴 정규식
578+
const urlRegex = /(https?:\/\/[^\s]+)/g;
579+
580+
// URL을 하이퍼링크 태그로 변환
581+
return content.replace(urlRegex, (url) => {
582+
return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`;
583+
});
584+
};
585+
576586
onMounted(() => {
577587
fetchQuestion()
578588
})

sciq-fe/src/views/BoardCreateView.vue

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { ref } from 'vue'
33
import { useRouter } from 'vue-router'
44
import { postService } from '@/api/postService'
5+
import RichTextEditor from '@/components/editor/RichTextEditor.vue'
56
67
const router = useRouter()
78
const title = ref('')
@@ -59,8 +60,8 @@ const validateForm = () => {
5960
}
6061
6162
// 내용 유효성 검사 (1000자 제한)
62-
if (contentLength > 1000) {
63-
contentError.value = '내용은 1000자 이하로 입력해주세요.'
63+
if (!content.value.trim()) {
64+
contentError.value = '내용을 입력해주세요.'
6465
isValid = false
6566
} else {
6667
contentError.value = ''
@@ -139,16 +140,9 @@ const handleCancel = () => {
139140
</div>
140141

141142
<div class="form-group">
142-
<textarea
143-
v-model="content"
144-
@input="checkContentLength"
145-
placeholder="내용을 입력하세요"
146-
class="content-input"
147-
:class="{ 'error': contentError }"
148-
></textarea>
143+
<RichTextEditor v-model="content" placeholder="내용을 입력하세요" />
149144
<div class="char-count">
150145
<div class="count-info">
151-
<span>{{ content.length }}/1000</span>
152146
<span v-if="contentError" class="error-message">{{ contentError }}</span>
153147
</div>
154148
</div>

0 commit comments

Comments
 (0)