yarn 으로 시작하기
1. yarn 설치
https://pakss328.medium.com/yarn이란-b4e8edf1638b
2. yarn global add @vue/cli-init
3. vue init webpack bootstrap-bbs
$ vue init webpack bootstrap-bbs ? Project name vue-board ? Project description A Vue.js project ? Author junhochoi ? Vue build standalone ? Install vue-router? Yes ? Use ESLint to lint your code? No ? Set up unit tests Yes ? Pick a test runner jest ? Setup e2e tests with Nightwatch? No ? Should we run `npm install` for you after the project has been created? (recommended) yarn vue-cli ·· Generated "vue-board".
main.js
// The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. import Vue from 'vue' import App from './App' import router from './router' import { BootstrapVue, IconsPlugin } from 'bootstrap-vue' import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue/dist/bootstrap-vue.css' // Make BootstrapVue available throughout your project Vue.use(BootstrapVue) // Optionally install the BootstrapVue icon components plugin Vue.use(IconsPlugin) Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', router, components: { App }, template: '<App/>' })
App.vue
<template> <div id="app"> <Header /> <!-- <img src="./assets/logo.png"> --> <router-view/> </div> </template> <script> import Header from '@/components/Header' export default { name: 'App', components:{ Header, } } </script> <style> #app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; } </style>
router/index.js
import Vue from 'vue' import Router from 'vue-router' import HelloWorld from '@/components/HelloWorld' import Board from '@/components/Board'; import ContentDetail from '@/components/ContentDetail'; import Create from '@/components/Create'; Vue.use(Router) export default new Router({ mode:"history", routes: [ { path: '/', name: 'HelloWorld', component: HelloWorld }, { path: '/board/free', name: 'Board', component: Board }, { path: '/board/free/detail/:contentId', name: 'ContentDetail', component: ContentDetail }, { path: '/board/free/create/:contentId?', name: 'Create', component: Create }, ] })
components/Header.vue
<template> <div> <b-navbar toggleable="lg" type="dark" variant="info"> <b-navbar-brand href="#">Vue.js로 게시판 만들기</b-navbar-brand> <b-navbar-toggle target="nav_collapse"/> <b-collapse is-nav id="nav_collapse"> <b-navbar-nav> <b-nav-item href="#">공지사항</b-nav-item> <b-nav-item to="/board/free">자유게시판</b-nav-item> <b-nav-item href="#">구인구직</b-nav-item> </b-navbar-nav> </b-collapse> </b-navbar> </div> </template> <script> export default { name: "Header", data() { return {}; } }; </script>
components/Board.vue
<template> <div> <b-table striped hover :items="items" :fields="fields" @row-clicked="rowClick"></b-table> <b-button @click="writeContent">글쓰기</b-button> </div> </template> <script> import data from '@/data'; export default { data() { let items=data.Content.sort((a,b)=>{ return b.content_id-a.content_id}) items=items.map(contentItem=>{ return {...contentItem, user_name:data.User.filter(userItem=>userItem.user_id===contentItem.user_id)[0].name}}) return { fields:[ { key: 'content_id', label: '글번호', }, { key: 'title', label: '제목', }, { key: 'created_at', label: '작성일', }, { key: 'user_name', label: '글쓴', } ], items:items } }, methods:{ rowClick(item, index, e){ this.$router.push({ path: `/board/free/detail/${item.content_id}` }); }, writeContent(){ this.$router.push({ path:'/board/free/create' }); } } } </script>
components/Create.vue
<template> <b-container fluid class="mt-2 p-5"> <b-row class="mt-2"> <b-col sm="12" > <b-input v-model="subject" placeholder="제목을 입력해 주세요" ></b-input> </b-col> <b-col sm="12" class="mt-2"> <b-form-textarea v-model="context" placeholder="내용을 입력해 주세요" rows="3" max-rows="10" ></b-form-textarea> </b-col> <b-col sm="12" class="mt-2"> <b-button @click="updateMode? updateContent():uploadContent() ">저장</b-button> <b-button @click="cancle">취소</b-button> </b-col> </b-row> </b-container> </template> <script> import data from '@/data'; export default { name:"Create", data(){ return { now: "00:00:00", subject:"", context:"", userId:1, createdAt:"", updatedAt:null, updateObject:null, updateMode:this.$route.params.contentId>0?true:false } }, created(){ if(this.$route.params.contentId>0){ const contentId=Number(this.$route.params.contentId); this.updateObject=data.Content.filter(item=>item.content_id===contentId)[0]; this.subject=this.updateObject.title; this.context=this.updateObject.context; this.createdAt=this.updateObject.created_at; } }, methods:{ uploadContent(){ let items=data.Content.sort((a,b)=>{return b.content_id-a.content_id}); const content_id=items[0].content_id+1; data.Content.push({ content_id: content_id, user_id: this.userId, title: this.subject, context: this.context, created_at: this.currentTime(), updated_at: null }); this.$router.push({ path:'/board/free/' }); }, updateContent(){ this.updateObject.title=this.subject; this.updateObject.context=this.context; this.updateObject.updated_at=this.currentTime(); this.$router.push({ path:'/board/free/' }); }, cancle(){ this.$router.push({ path:'/board/free/' }); }, currentTime(){ let today = new Date(); let year = today.getFullYear(); // 년도 let month = today.getMonth() + 1; // 월 let date = today.getDate(); // 날짜 let day = today.getDay(); // 요일 month=this.dateFormat(month); date=this.dateFormat(date); let currentDay=year + '-' + month + '-' + date; this.now = " "+this.dateFormat(today.getHours()) + ":" + this.dateFormat(today.getMinutes()) + ":" +this.dateFormat(today.getSeconds()); console.log(currentDay+this.now); return currentDay+this.now; }, dateFormat(str){ str= "00"+str; return str=str.slice(-2); } } } </script> <style> </style>
components/ContentDetail.vue
<template> <b-container fluid class="mt-5 p-5"> <b-row class="mt-2"> <b-col sm="2"> <label for="textarea-small">글번호:</label> </b-col> <b-col sm="10"> <b-form-input v-model="contentId" readonly ></b-form-input> </b-col> </b-row> <b-row class="mt-2"> <b-col sm="2"> <label for="textarea-small">글쓴이:</label> </b-col> <b-col sm="10"> <b-form-input v-model="user" readonly ></b-form-input> </b-col> </b-row> <b-row class="mt-2"> <b-col sm="2"> <label for="textarea-default">등록일:</label> </b-col> <b-col sm="10"> <b-form-input v-model="created" readonly ></b-form-input> </b-col> </b-row> <b-row class="mt-2"> <b-col sm="2"> <label for="textarea-default">수정일:</label> </b-col> <b-col sm="10"> <b-form-input v-model="updatedAt" readonly ></b-form-input> </b-col> </b-row> <b-row class="mt-2"> <b-col sm="2"> <label for="textarea-large">내용:</label> </b-col> <b-col sm="10"> <b-form-textarea size="lg" rows="10" v-model="context" ></b-form-textarea> </b-col> </b-row> <b-row class="mt-2"> <b-col sm="12" class="text-center"> <b-button variant="primary" @click="updateData">수정</b-button> <b-button variant="success" @click="deleteData">삭제</b-button> </b-col> </b-row> <b-row class="mt-5"> <b-col sm="12 text-left"><span>댓글</span></b-col> </b-row> <b-row class="mb-5 comment"> <b-col sm="12"> <CommentList :contentId="contentId"/> </b-col> </b-row> </b-container> </template> <script> import data from '@/data'; import CommentList from '@/components/CommentList'; export default{ name:"ContentDetail", components:{ CommentList }, data(){ const contentId=Number(this.$route.params.contentId); const contentData=data.Content.filter(item=>item.content_id===contentId)[0] return{ contentId:contentId, title:contentData.title, context:contentData.context, user:data.User.filter(item=>item.user_id===contentData.user_id)[0].name, created:contentData.created_at, updatedAt:contentData.updated_at } }, methods:{ updateData(){ this.$router.push({ path: `/board/free/create/${this.contentId}` }) }, deleteData(){ const content_index = data.Content.findIndex(item => item.content_id === this.contentId); data.Content.splice(content_index, 1) this.$router.push({ path: '/board/free' }); } } } </script> <style scoped> .comment{ border: 1px solid black; margin-top: 1rem; padding: 2rem; } </style>
components/CommentList.vue
<template> <div> <div v-for="(item, index) in comments" :key="item.comment_id"> <div> this index : {{index}} <CommentListItem :commentObject="item" :reloadComment="reloadComment" :commentIndex="index" /> </div> </div> <div class="mb-5"> <CommentCreate :contentId="contentId" :reloadComment="reloadComment" /> </div> </div> </template> <script> import data from '@/data'; import CommentListItem from "@/components/CommentListItem"; import CommentCreate from "@/components/CommentCreate"; export default { name:"CommentList", props:{ contentId:Number, }, components:{ CommentListItem, CommentCreate, }, data(){ return { comments:data.Comment.filter(item => item.content_id === this.contentId) } }, methods:{ reloadComment(){ this.comments=data.Comment.filter(item => item.content_id === this.contentId) } } } </script> <style> </style>
components/CommentCreate.vue
<template> <div> <b-input-group :prepend="name" class="mt-3"> <b-form-textarea v-model="context" placeholder="코멘트를 달아 주세" rows="3" max-rows="6" ></b-form-textarea> <b-input-group-append> <b-button variant="info" @click="isSubComment ? createSubComment():createComment()">작성하기</b-button> </b-input-group-append> </b-input-group> </div> </template> <script> import data from "@/data"; export default { name:"CommentCreate", props:{ contentId:Number, reloadComment:Function, commentId:Number, isSubComment:Boolean, reloadSubComments:Function, subCommentToggle:Function }, data(){ return { name:"르라나", context:'' } }, methods:{ createComment(){ data.Comment.push( { comment_id:data.Comment[data.Comment.length-1].comment_id+1, user_id:1, content_id:this.contentId, context:this.context, created_at:this.currentTime(), updated_at:null }); this.reloadComment(); this.context=""; }, createSubComment(){ let subCommentobj={ subcomment_id:data.SubComment[data.SubComment.length-1].subcomment_id+1, user_id:1, comment_id:this.commentId, context:this.context, created_at:this.currentTime(), updated_at:null } data.SubComment.push( subCommentobj ); this.reloadSubComments(); this.context=""; this.subCommentToggle(); }, currentTime(){ let today = new Date(); let year = today.getFullYear(); // 년도 let month = today.getMonth() + 1; // 월 let date = today.getDate(); // 날짜 let day = today.getDay(); // 요일 month=this.dateFormat(month); date=this.dateFormat(date); let currentDay=year + '-' + month + '-' + date; this.now = " "+this.dateFormat(today.getHours()) + ":" + this.dateFormat(today.getMinutes()) + ":" +this.dateFormat(today.getSeconds()); console.log(currentDay+this.now); return currentDay+this.now; }, dateFormat(str){ str= "00"+str; return str=str.slice(-2); } } } </script> <style> </style>
components/CommentListItem.vue
<template> <div> <div class="comment-list-item"> <div class="comment-list-item-name"> <div>{{name}}</div> <div>댓글 번호: {{commentId}}</div> <div>{{commentObject.created_at}}</div> </div> <div class="comment-list-item-context">{{commentObject.context}}</div> <div class="comment-list-item-button"> <b-button variant="info" @click="modalCommentBtn(commentObject)">수정</b-button> <b-button variant="info" @click="commentDelete">삭제</b-button> <b-button variant="info" @click="subCommentToggle">덧글 달기</b-button> </div> </div> <template v-if="subCommentCreateToggle" > <SubCommentCreate class="mb-3" :commentId="commentId" :isSubComment="true" :reloadSubComments="reloadSubComments" :subCommentToggle="subCommentToggle" /> </template> <template v-if="subCommentList.length >0"> <div v-for="item in subCommentList" :key="item.subcomment_id" class="comment-list-item-subcomment-list "> <div class="comment-list-item"> <div class="comment-list-item-name"> <div>{{item.user_name}}</div> <div>댓글 번호: {{item.comment_id}}</div> <div>서브 댓글 번호: {{item.subcomment_id}}</div> <div>{{item.created_at}}</div> </div> <div class="comment-list-item-context">{{item.context}}</div> <div class="comment-list-item-button"> <b-button variant="info" @click="modalSubCommentBtn(item)" >수정</b-button> <b-button variant="info" @click="commentSubDelete(item.subcomment_id)">삭제</b-button> </div> </div> </div> </template> <div> <CommentUpdateModal ref="commentUpdateModal" :reloadComment="reloadComment" :commentObject="commentObject" :commentIndex="commentIndex" /> </div> </div> </template> <script> import data from "@/data"; import SubCommentCreate from "@/components/CommentCreate"; import CommentUpdateModal from "@/Components/CommentUpdateModal"; export default { name:"CommentListItem", props:{ commentObject:Object, reloadComment:Function, commentIndex:Number, }, components:{ SubCommentCreate, CommentUpdateModal }, data(){ return{ commentId:this.commentObject.comment_id, modalCommentObject:this.commentObject, // userId:this.commentObject.user_id, // contentId:this.commentObject.content_id, // updatedAt:this.commentObject.updated_at, name:data.User.filter(item=> item.user_id===this.commentObject.user_id)[0].name, subCommentList:data.SubComment.filter( item=>item.comment_id===this.commentObject.comment_id ).map(subCommentItem =>{return { ...subCommentItem, user_name:data.User.filter(item=> item.user_id===subCommentItem.user_id)[0].name, }}), subCommentCreateToggle:false, } }, methods:{ subCommentToggle(){ this.subCommentCreateToggle=!this.subCommentCreateToggle; }, reloadSubComments(){ this.subCommentList=data.SubComment.filter( item=>item.comment_id===this.commentObject.comment_id ).map(subCommentItem =>{return { ...subCommentItem, user_name:data.User.filter(item=> item.user_id===subCommentItem.user_id)[0].name, }}) }, commentDelete(){ if(confirm("정말 삭제 하시겠습니까?")){ let commentId=this.commentObject.comment_id; let findItem=data.Comment.find(function(item){ return item.comment_id===commentId}); let idx=data.Comment.indexOf(findItem); console.log("findItem:"); console.dir(findItem); console.log("idx:"+idx); data.Comment.splice(idx, 1); this.reloadComment(); } }, commentSubDelete(subcommentId){ if(confirm("정말 삭제 하시겠습니까?")){ let findItem=data.SubComment.find(function(item){ return item.subcomment_id==subcommentId}); let idx=data.SubComment.indexOf(findItem); data.SubComment.splice(idx, 1); this.reloadSubComments(); } }, modalCommentBtn(commentObject){ this.$refs.commentUpdateModal.$refs['my-modal'].hide(); this.$refs.commentUpdateModal.$refs['my-modal'].show(); }, } } </script> <style scoped> .comment-list-item { display: flex; justify-content: space-between; padding-bottom: 1em; } .comment-list-item-name { display: flex; flex-direction: column; justify-content: center; align-items: center; border: 0.5px solid black; padding: 1em; } .comment-list-item-context { display: flex; justify-content: center; align-items: center; width: 50em; border: 0.5px solid black; } .comment-list-item-button { display: flex; flex-direction: column; justify-content: center; align-items: center; border: 0.5px solid black; padding: 1em; } .btn { margin-bottom: 1em; } .comment-list-item-subcomment-list { display: flex; justify-content: space-between; padding-bottom: 1em; margin-left: 10em; } </style>
components/CommentUpdateModal.vue
<template> <div> <b-modal id="commentUpdate-modal" ref="my-modal" title="수정 하기" @show="resetModal" @hidden="resetModal" @ok="handleOk" > <form ref="form" @submit.stop.prevent="handleSubmit"> <b-form-input ref="modalCommentId" v-model="commentId" :state="commentIdState" required hidden ></b-form-input> <b-form-textarea ref="modalContext" v-model="context" :state="contextState" required ></b-form-textarea> </form> </b-modal> <b-modal id="commentSubUpdate-modal" ref="my-sub-modal" title="서브 댓글 수정 하기" @show="resetModal" @hidden="resetModal" @ok="handleOk" > <form ref="form" @submit.stop.prevent="handleSubSubmit"> <b-form-textarea ref="subContext" id="sub_context" v-model="context" required ></b-form-textarea> </form> </b-modal> </div> </template> <script> import data from "@/data"; export default { name:"CommentUpdateModal", props:{ reloadComment:Function, commentObject:Object, commentIndex:Number , reloadSubComments:Function }, data() { return { context: this.commentObject.context, commentId:this.commentObject.comment_id, contextState: null, commentIdState:null, } }, methods: { checkFormValidity() { const valid = this.$refs.form.checkValidity() this.contextState = valid return valid }, resetModal() { }, handleOk(bvModalEvt) { // Prevent modal from closing bvModalEvt.preventDefault() // Trigger submit handler this.handleSubmit() }, handleSubmit() { // Exit when the form isn't valid if (!this.checkFormValidity()) { alert("댓글을 작성해 주세요."); this.$refs.modalContext.focus; return } //데이타 업데이트 let commentId=this.commentId; data.Comment.filter(item=>item.comment_id===commentId)[0].context=this.context; this.reloadComment(); //모달 닫 this.$nextTick(() => { this.$bvModal.hide('commentUpdate-modal'); }); }, handleSubSubmit() { // Exit when the form isn't valid if (!this.checkFormValidity()) { alert("댓글을 작성해 주세요."); this.$refs.modalContext.focus; return } }, } } </script>
댓글 ( 6)
댓글 남기기