write 수정 컴포넌트 추가

This commit is contained in:
ckx6954 2024-12-17 09:09:56 +09:00
parent 8034f1fc37
commit fb26f6d701
9 changed files with 282 additions and 188 deletions

46
package-lock.json generated
View File

@ -2701,12 +2701,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/eventemitter3": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
"integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==",
"license": "MIT"
},
"node_modules/execa": { "node_modules/execa": {
"version": "9.5.2", "version": "9.5.2",
"resolved": "https://registry.npmjs.org/execa/-/execa-9.5.2.tgz", "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.2.tgz",
@ -3732,12 +3726,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/parchment": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==",
"license": "BSD-3-Clause"
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -3997,20 +3985,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/quill": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz",
"integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
"license": "BSD-3-Clause",
"dependencies": {
"clone": "^2.1.1",
"deep-equal": "^1.0.1",
"eventemitter3": "^2.0.3",
"extend": "^3.0.2",
"parchment": "^1.1.4",
"quill-delta": "^3.6.2"
}
},
"node_modules/quill-delta": { "node_modules/quill-delta": {
"version": "4.2.2", "version": "4.2.2",
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-4.2.2.tgz", "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-4.2.2.tgz",
@ -4028,26 +4002,6 @@
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/quill/node_modules/fast-diff": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==",
"license": "Apache-2.0"
},
"node_modules/quill/node_modules/quill-delta": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
"integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
"license": "MIT",
"dependencies": {
"deep-equal": "^1.0.1",
"extend": "^3.0.2",
"fast-diff": "1.1.2"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.3", "version": "1.5.3",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz",

View File

@ -1,69 +0,0 @@
<template>
<Editor :api-key="editorKey" :init="init" />
</template>
<script setup>
import Editor from '@tinymce/tinymce-vue';
import { reactive } from 'vue';
const editorKey = '71nhna4fusscfsf8qvo9cg3ul4uydwt346izxa6ra6zwjsz7';
const init = reactive({
toolbar_mode: 'sliding',
plugins: [
// Core editing features
'anchor',
'autolink',
'charmap',
'codesample',
'emoticons',
'image',
'link',
'lists',
'media',
'searchreplace',
'table',
'visualblocks',
'wordcount',
// Your account includes a free trial of TinyMCE premium features
// Try the most popular premium features until Dec 30, 2024:
'checklist',
'mediaembed',
'casechange',
'export',
'formatpainter',
'pageembed',
'a11ychecker',
'tinymcespellchecker',
'permanentpen',
'powerpaste',
'advtable',
'advcode',
'editimage',
'advtemplate',
'ai',
'mentions',
'tinycomments',
'tableofcontents',
'footnotes',
'mergetags',
'autocorrect',
'typography',
'inlinecss',
],
toolbar:
'undo redo | blocks fontfamily fontsize | bold italic underline strikethrough | link image media table mergetags | addcomment showcomments | spellcheckdialog a11ycheck typography | align lineheight | checklist numlist bullist indent outdent | emoticons charmap | removeformat',
tinycomments_mode: 'embedded',
tinycomments_author: 'Author name',
mergetags_list: [
{ value: 'First.Name', title: 'First Name' },
{ value: 'Email', title: 'Email' },
],
ai_request: (request, respondWith) => respondWith.string(() => Promise.reject('See docs to implement AI Assistant')),
});
</script>
<style>
.tox-statusbar{
display: none !important;
}
</style>

View File

@ -0,0 +1,153 @@
<template>
<Editor :api-key="editorKey" :init="init" />
</template>
<script setup>
import Editor from '@tinymce/tinymce-vue';
import { nextTick, onBeforeUnmount, onMounted, reactive } from 'vue';
const editorKey = '71nhna4fusscfsf8qvo9cg3ul4uydwt346izxa6ra6zwjsz7';
const init = reactive({
menubar: false,
toolbar_mode: 'sliding',
selector: 'textarea',
plugins: [
'code',
'anchor',
'autolink',
'charmap',
'codesample',
'emoticons',
'image',
'link',
'lists',
'media',
'searchreplace',
'table',
'visualblocks',
'wordcount',
],
//
statusbar: false,
//code sample
codesample_content_css: 'pre { white-space: pre-wrap; word-wrap: break-word; }',
tabsize: 4,
tabfocus: false,
tabnavigation: false,
//
toolbar: [
'undo redo | fontfamily fontsize | blockquote h1 h2 h3 h4 h5 h6 | bold italic underline alignleft aligncenter alignright alignjustify indent outdent',
'checklist numlist bullist | link image table | codesample emoticons code',
],
style_format_merge: true,
formats: {
blockquote: {
block: 'blockquote',
styles: { 'font-style': 'italic', 'border-left': '4px solid #ccc', 'padding-left': '10px' },
},
paragraph: {
block: 'p',
styles: { 'margin-bottom': '1em', 'font-size': '1em' },
},
heading1: {
block: 'h1',
styles: { 'font-size': '2em', 'font-weight': 'bold', 'margin-top': '1em', 'margin-bottom': '0.5em' },
},
heading2: {
block: 'h2',
styles: { 'font-size': '1.75em', 'font-weight': 'bold', 'margin-top': '1em', 'margin-bottom': '0.5em' },
},
heading3: {
block: 'h3',
styles: { 'font-size': '1.5em', 'font-weight': 'bold', 'margin-top': '1em', 'margin-bottom': '0.5em' },
},
heading4: {
block: 'h4',
styles: { 'font-size': '1.25em', 'font-weight': 'bold', 'margin-top': '1em', 'margin-bottom': '0.5em' },
},
heading5: {
block: 'h5',
styles: { 'font-size': '1em', 'font-weight': 'bold', 'margin-top': '1em', 'margin-bottom': '0.5em' },
},
heading6: {
block: 'h6',
styles: { 'font-size': '0.875em', 'font-weight': 'bold', 'margin-top': '1em', 'margin-bottom': '0.5em' },
},
alignleft: {
selector: 'p,h1,h2,h3,h4,h5,h6,div',
styles: { 'text-align': 'left' },
},
aligncenter: {
selector: 'p,h1,h2,h3,h4,h5,h6,div',
styles: { 'text-align': 'center' },
},
alignright: {
selector: 'p,h1,h2,h3,h4,h5,h6,div',
styles: { 'text-align': 'right' },
},
alignjustify: {
selector: 'p,h1,h2,h3,h4,h5,h6,div',
styles: { 'text-align': 'justify' },
},
},
setup(editor) {
editor.on('init', async () => {
//
await wait(500);
const targetElement = document.querySelector('[aria-label="Insert/edit code sample"]');
targetElement.addEventListener('click', async () => {
//
await wait(200);
const textEle = document.querySelector('.tox-textarea-wrap');
const textarea = textEle.querySelector('textarea');
if (textarea) {
// tab
const handleTabKey = event => {
if (event.key === 'Tab') {
event.preventDefault(); // ( )
//
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
// 4
const text = textarea.value;
textarea.value = text.substring(0, start) + ' ' + text.substring(end);
const newCursorPos = start + 4;
//
setTimeout(() => {
textarea.focus(); //
//
textarea.selectionStart = textarea.selectionEnd = newCursorPos;
}, 0);
}
};
//
textarea.removeEventListener('keydown', handleTabKey);
// keydown
textarea.addEventListener('keydown', handleTabKey);
}
});
});
},
});
const wait = timeToDelay => new Promise(resolve => setTimeout(resolve, timeToDelay));
// emit file ~
</script>
<style scoped>
</style>

View File

@ -1,11 +1,17 @@
<template> <template>
<div class="mb-4 row"> <div class="mb-4 row">
<label for="html5-text-input" class="col-md-2 col-form-label"> <label :for="name" class="col-md-2 col-form-label">
{{ $props.title }} {{ title }}
<span :class="$props.isEssential ? 'text-red' : 'none'">*</span> <span :class="isEssential ? 'text-red' : 'none'">*</span>
</label> </label>
<div class="col-md-10"> <div class="col-md-10">
<input class="form-control" :type="$props.type" @input="updateInput" :value="$props.value" :maxLength="$props.maxlength" /> <input :id="name"
class="form-control"
:type="type"
@input="updateInput"
:value="value"
:maxLength="maxlength"
:placeholder="title" />
</div> </div>
</div> </div>
</template> </template>
@ -19,6 +25,11 @@ const prop = defineProps({
default: '라벨', default: '라벨',
required: true, required: true,
}, },
name: {
type: String,
default: 'nameplz',
required: true,
},
isEssential: { isEssential: {
type: Boolean, type: Boolean,
default: false, default: false,
@ -44,7 +55,6 @@ const prop = defineProps({
const emits = defineEmits(['inputVal']) const emits = defineEmits(['inputVal'])
const updateInput = function (event) { const updateInput = function (event) {
//Type Number maxlength //Type Number maxlength
if (event.target.value.length > prop.maxlength) { if (event.target.value.length > prop.maxlength) {
event.target.value = event.target.value.slice(0, prop.maxlength); event.target.value = event.target.value.slice(0, prop.maxlength);

View File

@ -1,20 +1,56 @@
<template> <template>
<div class="mb-4 row"> <div class="mb-4 row">
<label for="html5-email-input" class="col-md-2 col-form-label"> <label for="input-ss" class="col-md-2 col-form-label">
카테고리 {{ title }}
<span class="text-red">*</span> <span :class="isEssential ? 'text-red' : 'none'">*</span>
</label> </label>
<div class="col-md-10"> <div class="col-md-10">
<select class="form-select" id="exampleFormControlSelect1" aria-label="Default select example"> <select class="form-select" id="input-ss" v-model="selectData">
<option selected value="1">자유</option> <option v-for="(item , i) in data" :key="item" :value="i" :selected = 'value == i' >{{ item }}</option>
<option value="1">임시</option>
<option value="2">공지</option>
</select> </select>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, watchEffect } from 'vue';
const prop = defineProps({
title: {
type: String,
default: '라벨',
required: true,
},
name: {
type: String,
default: 'nameplz',
required: true,
},
isEssential: {
type: Boolean,
default: false,
required: false,
},
data: {
type: Array,
default: [],
required: true,
},
value: {
type: String,
default: '0',
require: false,
},
});
const emit = defineEmits(['selectVal'])
const selectData = ref(prop.value);
watchEffect(() => {
emit('selectVal',selectData);
})
</script> </script>
<style> <style>

View File

@ -3,47 +3,38 @@ import BoardWrite from '@v/board/BoardWrite.vue';
// 초기 렌더링 속도를 위해 지연 로딩 사용 // 초기 렌더링 속도를 위해 지연 로딩 사용
const routes = [ const routes = [
{ {
path: '/', path: '/',
component: () => import('@v/MainView.vue'), component: () => import('@v/MainView.vue'),
// meta: { requiresAuth: true } // meta: { requiresAuth: true }
}, },
{ {
path: '/board', path: '/board',
component: () => import('@v/board/TheBoard.vue'), component: () => import('@v/board/TheBoard.vue'),
children : [ children: [
{ {
path : '', path: '',
component : () => import('@v/board/BoardList.vue') component: () => import('@v/board/BoardList.vue')
}, },
{ {
path : 'write', path: 'write',
component : () => import('@v/board/BoardWrite.vue') component: () => import('@v/board/BoardWrite.vue')
}, },
{ {
path : 'get/:id', path: 'get/:id',
component : () => import('@v/board/BoardView.vue') component: () => import('@v/board/BoardView.vue')
} }
] ]
}, },
// { {
// path: "/login", path: "/:anything(.*)",
// component: () => import('@v/Login.vue'), component: () => import('@v/ErrorPage.vue'),
// // meta: { requiresAuth: false } }
// }, ];
// {
// path : "/error/:code",
// component: () => import('@v/ErrorPage.vue'),
// },
{
// path: '/:anything(.*)',
// component: () => import('@v/MainView.vue'),
},
]
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes: routes, routes: routes,
}) })
export default router export default router

13
src/views/ErrorPage.vue Normal file
View File

@ -0,0 +1,13 @@
<template>
Error
</template>
<script>
export default {
}
</script>
<style>
</style>

View File

@ -7,33 +7,29 @@
</div> </div>
</div> </div>
<div class="col-xl-6"> <div class="col-xl-12">
<!-- HTML5 Inputs --> <div class="card-body">
<div class="card mb-6"> <FormInput title="제목" name="title" :is-essential="true" @input-val="title = $event" />
<div class="card-body">
<FormInput title="제목" :is-essential="true" @input-val="title = $event"/>
<FormSelect/> <FormSelect title="카테고리" name="cate" :is-essential="true" :data="categoryList" @select-val="category = $event" />
<FormInput title="비밀번호" type="password" :is-essential="true" @input-val="password = $event"/> <FormInput title="비밀번호" name="pw" type="password" :is-essential="true" @input-val="password = $event" />
<FormFile /> <FormFile />
<div class="mb-4"> <div class="mb-4">
<label for="html5-tel-input" class="col-md-2 col-form-label"> <label for="html5-tel-input" class="col-md-2 col-form-label">
내용 내용
<span class="text-red">*</span> <span class="text-red">*</span>
</label> </label>
<div class="col-md-12"> <div class="col-md-12">
<QEditor /> <TEditor />
</div>
</div>
<div class="mb-4 d-flex justify-content-end">
<button type="button" class="btn btn-info right">목록</button>
<button type="button" class="btn btn-primary ms-1">작성</button>
</div> </div>
</div>
<div class="mb-4 d-flex justify-content-end">
<button type="button" class="btn btn-info right">목록</button>
<button type="button" class="btn btn-primary ms-1">작성</button>
</div> </div>
</div> </div>
</div> </div>
@ -42,14 +38,17 @@
</template> </template>
<script setup> <script setup>
import QEditor from '@c/editor/QEditor.vue'; import TEditor from '@c/editor/TEditor.vue';
import FormInput from '@c/input/FormInput.vue'; import FormInput from '@c/input/FormInput.vue';
import FormSelect from '@c/input/FormSelect.vue'; import FormSelect from '@c/input/FormSelect.vue';
import FormFile from '@c/input/FormFile.vue'; import FormFile from '@c/input/FormFile.vue';
import { ref } from 'vue'; import { ref } from 'vue';
const categoryList = ['자유', '임시', '공지사항'];
const title = ref(''); const title = ref('');
const password = ref(''); const password = ref('');
const category = ref('');
</script> </script>
<style> <style>

View File

@ -1,3 +1,10 @@
<template> <template>
<RouterView /> <RouterView />
</template> </template>
<script>
export default {
name : 'board',
inheritAttrs : false,
}
</script>